@kortix/ui 0.14.10 → 0.14.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/_vendor/shared/runtime-versions.d.ts +9 -0
  2. package/dist/browser/browser-panel.d.ts +2 -1
  3. package/dist/browser/browser-panel.js +12 -2
  4. package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
  5. package/dist/files/renderers/show-content-renderer.d.ts +2 -2
  6. package/dist/files/renderers/show-content-renderer.js +2 -1
  7. package/dist/i18n/en.d.ts +18 -0
  8. package/dist/i18n/en.js +30 -12
  9. package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
  10. package/dist/internal/branding/branding-provider.js +3 -3
  11. package/dist/internal/hooks/use-notification-center.d.ts +10 -6
  12. package/dist/internal/hooks/use-notification-center.js +32 -10
  13. package/dist/internal/lib/browser-noise/rules/extensions.js +3 -0
  14. package/dist/internal/lib/site-metadata.js +4 -4
  15. package/dist/internal/notifications/notification-bell.js +4 -3
  16. package/dist/internal/notifications/notification-rows.d.ts +1 -1
  17. package/dist/internal/notifications/notification-rows.js +2 -0
  18. package/dist/internal/stores/kortix-computer-store.d.ts +3 -2
  19. package/dist/internal/stores/kortix-computer-store.js +5 -3
  20. package/dist/session/composer/index.d.ts +1 -0
  21. package/dist/session/composer/index.js +1 -0
  22. package/dist/session/composer/send-stop-control.js +8 -3
  23. package/dist/session/header/session-site-header.js +3 -1
  24. package/dist/session/panel/easy/detail-view.js +11 -0
  25. package/dist/session/panel/mobile-tool-drawer.js +10 -4
  26. package/dist/session/panel/open-session-quick-view.js +3 -3
  27. package/dist/session/panel/panel-fallback.js +10 -7
  28. package/dist/session/panel/session-desktop.d.ts +6 -0
  29. package/dist/session/panel/session-desktop.js +74 -0
  30. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  31. package/dist/session/panel/session-panel-provider.js +17 -1
  32. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  33. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  34. package/dist/session/panel/use-session-panel-layout.js +8 -0
  35. package/dist/session/status/session-tab-title.d.ts +1 -1
  36. package/dist/session/status/session-tab-title.js +1 -1
  37. package/dist/session/thread/session-chat.js +12 -0
  38. package/dist/session/thread/session-context-sub-sessions.js +1 -1
  39. package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
  40. package/dist/session/tool/tools/image-gen-tool.js +17 -8
  41. package/dist/session/turn/user-message.d.ts +5 -4
  42. package/dist/session/turn/user-message.js +7 -5
  43. package/dist/styles.css +1 -1
  44. package/dist/workspace/session-workspace.js +24 -5
  45. package/package.json +3 -3
  46. package/skills/kortix-ui/references/modules.md +1 -1
@@ -19,6 +19,10 @@ export type RuntimeVersions = {
19
19
  agentBrowser: string;
20
20
  playwright: string;
21
21
  anydoc: string;
22
+ /** tembo/voxl commit the sandbox desktop streamer is built from (apps/sandbox/voxl/release.sh). */
23
+ voxl: string;
24
+ voxlSha256Amd64: string;
25
+ voxlSha256Arm64: string;
22
26
  /** pi packages every pi session loads, as pi sources: `npm:<name>@<exact version>`. */
23
27
  piSystemPackages: string[];
24
28
  };
@@ -45,6 +49,11 @@ export declare const OPENCODE_USER_AGENT: string;
45
49
  export declare const AGENT_BROWSER_VERSION: string;
46
50
  export declare const PLAYWRIGHT_VERSION: string;
47
51
  export declare const ANYDOC_VERSION: string;
52
+ export declare const VOXL_VERSION: string;
53
+ export declare const VOXL_SHA256_AMD64: string;
54
+ export declare const VOXL_SHA256_ARM64: string;
55
+ /** The GitHub release apps/sandbox/voxl/release.sh publishes for VOXL_VERSION. */
56
+ export declare const VOXL_RELEASE_URL: string;
48
57
  /** Throws unless `source` is an exact `npm:<name>@<x.y.z>` pin. */
49
58
  export declare function assertPiSystemPackage(source: string): void;
50
59
  /**
@@ -2,6 +2,7 @@ interface PreviewTabContentProps {
2
2
  tabId: string;
3
3
  projectId?: string;
4
4
  projectSessionId?: string;
5
+ targetUrl?: string;
5
6
  }
6
7
  /**
7
8
  * Preview tab content — renders a proxied sandbox URL in an iframe
@@ -11,5 +12,5 @@ interface PreviewTabContentProps {
11
12
  * any localhost:PORT address to navigate within the sandbox. Visited URLs are
12
13
  * recorded and offered back as "Recents" on the empty landing state.
13
14
  */
14
- export declare function BrowserPanel({ tabId, projectId, projectSessionId }: PreviewTabContentProps): import("react").JSX.Element;
15
+ export declare function BrowserPanel({ tabId, projectId, projectSessionId, targetUrl }: PreviewTabContentProps): import("react").JSX.Element;
15
16
  export {};
@@ -43,7 +43,7 @@ const subscribeNoop = () => () => { };
43
43
  * any localhost:PORT address to navigate within the sandbox. Visited URLs are
44
44
  * recorded and offered back as "Recents" on the empty landing state.
45
45
  */
46
- export function BrowserPanel({ tabId, projectId, projectSessionId }) {
46
+ export function BrowserPanel({ tabId, projectId, projectSessionId, targetUrl }) {
47
47
  const tHardcodedUi = useTranslations('hardcodedUi');
48
48
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
49
49
  const appPreviewTitle = tI18nComplete.raw('text6026c1d8c92e');
@@ -69,7 +69,7 @@ export function BrowserPanel({ tabId, projectId, projectSessionId }) {
69
69
  // reveal scroll shoves a permanent sideways offset onto the panel's
70
70
  // overflow-hidden ancestors. Mount-only on purpose (`autoFocus` semantics —
71
71
  // a later navigation clearing the URL must not steal the cursor).
72
- const autoFocusAddressRef = useRef(!rawPreviewUrl);
72
+ const autoFocusAddressRef = useRef(!rawPreviewUrl && !targetUrl);
73
73
  useEffect(() => {
74
74
  if (autoFocusAddressRef.current)
75
75
  focusWithoutScroll(addressInputRef.current);
@@ -200,6 +200,16 @@ export function BrowserPanel({ tabId, projectId, projectSessionId }) {
200
200
  pushHistory(newProxyUrl);
201
201
  handleRefresh();
202
202
  }, [subdomainOpts, rewritePortPath, tabId, appPreviewTitle, updateTabMetadata, pushHistory, handleRefresh]);
203
+ const appliedTarget = useRef(null);
204
+ useEffect(() => {
205
+ if (!targetUrl || (appliedTarget.current?.tabId === tabId && appliedTarget.current.url === targetUrl))
206
+ return;
207
+ appliedTarget.current = { tabId, url: targetUrl };
208
+ // An external quick-view intent synchronizes the Browser tab and its navigation history.
209
+ // eslint-disable-next-line react-hooks/set-state-in-effect
210
+ if (targetUrl !== rawPreviewUrl && targetUrl !== originalUrl)
211
+ navigateTo(targetUrl);
212
+ }, [tabId, targetUrl, rawPreviewUrl, originalUrl, navigateTo]);
203
213
  const canGoBack = historyIndex > 0;
204
214
  const canGoForward = historyIndex < history.length - 1;
205
215
  /** Walk the history stack in either direction; the two directions were two
@@ -13,6 +13,7 @@ import { ShippingContainerIcon as Container, FileCodeIcon as FileCode, PackageIc
13
13
  import { useMutation, useQueryClient } from '@tanstack/react-query';
14
14
  import { useMemo, useState } from 'react';
15
15
  import { Button } from '../../../primitives/button.js';
16
+ import { Checkbox } from '../../../primitives/checkbox.js';
16
17
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../../primitives/dialog.js';
17
18
  import { Input } from '../../../primitives/input.js';
18
19
  import { Label } from '../../../primitives/label.js';
@@ -48,6 +49,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
48
49
  const [cpu, setCpu] = useState('2');
49
50
  const [memoryGb, setMemoryGb] = useState('4');
50
51
  const [diskGb, setDiskGb] = useState('20');
52
+ const [residentialProxy, setResidentialProxy] = useState(false);
51
53
  // Reset / hydrate when opening
52
54
  const [hydratedFor, setHydratedFor] = useState(null);
53
55
  if (!hydratedFor || hydratedFor[0] !== open || hydratedFor[1] !== template) {
@@ -70,6 +72,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
70
72
  setCpu(String(template.cpu));
71
73
  setMemoryGb(String(template.memory_gb));
72
74
  setDiskGb(String(template.disk_gb));
75
+ setResidentialProxy(!!template.residential_proxy);
73
76
  }
74
77
  else if (open) {
75
78
  setName('');
@@ -82,6 +85,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
82
85
  setCpu('2');
83
86
  setMemoryGb('6');
84
87
  setDiskGb('20');
88
+ setResidentialProxy(false);
85
89
  }
86
90
  }
87
91
  // Auto-slug from name when the user hasn't typed a slug manually yet.
@@ -117,6 +121,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
117
121
  cpu: parsePosInt(cpu),
118
122
  memory_gb: parsePosInt(memoryGb),
119
123
  disk_gb: parsePosInt(diskGb),
124
+ ...(residentialProxy ? { residential_proxy: true } : {}),
120
125
  }),
121
126
  onSuccess: () => {
122
127
  successToast(t('toasts.created'));
@@ -135,6 +140,8 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
135
140
  cpu: parsePosInt(cpu) ?? null,
136
141
  memory_gb: parsePosInt(memoryGb) ?? null,
137
142
  disk_gb: parsePosInt(diskGb) ?? null,
143
+ // Keep a country set through kortix.yaml or the API while it stays on.
144
+ residential_proxy: residentialProxy ? (template?.residential_proxy ?? true) : null,
138
145
  }),
139
146
  onSuccess: () => {
140
147
  successToast(t('toasts.updated'));
@@ -148,7 +155,7 @@ export function SandboxTemplateForm({ projectId, open, onOpenChange, template, }
148
155
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(Container, { className: "size-4" }), isEdit ? t('editTitle', { name: template?.name ?? '' }) : t('newTitle')] }), _jsx(DialogDescription, { children: t('description') })] }), _jsxs("div", { className: "space-y-4 py-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "tpl-name", children: t('name') }), _jsx(Input, { id: "tpl-name", placeholder: t('namePlaceholder'), value: name, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "tpl-slug", children: t('slug') }), _jsx(Input, { id: "tpl-slug", placeholder: tI18nComplete.raw('text5d58d41913d9'), value: slug, onChange: (e) => {
149
156
  setSlugManuallyEdited(true);
150
157
  setSlug(e.target.value.toLowerCase());
151
- }, disabled: isEdit, "aria-invalid": !!slugError }), slugError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: slugError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('imageSource') }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2", children: [_jsxs("button", { type: "button", onClick: () => setMode('image'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'image' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Package, { className: "size-4" }), _jsx("span", { className: "font-medium", children: t('publicImage') })] }), _jsxs("span", { className: "text-muted-foreground text-xs", children: [t('example'), " ", _jsx("code", { className: "font-mono", children: "python:3.12-slim" })] })] }), _jsxs("button", { type: "button", onClick: () => setMode('dockerfile'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'dockerfile' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FileCode, { className: "size-4" }), _jsx("span", { className: "font-medium", children: tI18nComplete.raw('textdd2c0eb6ea5c') })] }), _jsx("span", { className: "text-muted-foreground text-xs", children: t('pathInsideRepo') })] })] }), _jsxs("div", { className: "mt-3", children: [mode === 'image' ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-image", children: t('image') }), _jsx(Input, { id: "tpl-image", placeholder: tI18nComplete.raw('text1d0447543fac'), value: image, onChange: (e) => setImage(e.target.value) }), _jsxs("p", { className: "text-muted-foreground mt-1 text-xs", children: [t('specificTag'), " (", _jsx("code", { className: "font-mono", children: "latest" }), ")."] })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-df", children: t('dockerfilePath') }), _jsx(Input, { id: "tpl-df", placeholder: tI18nComplete.raw('text1bc7a47dada1'), value: dockerfilePath, onChange: (e) => setDockerfilePath(e.target.value) }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: t('relativeToRoot') })] })), sourceError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: sourceError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('resources') }), _jsxs("div", { className: "mt-2 grid grid-cols-3 gap-3", children: [_jsx(NumericField, { id: "cpu", label: tI18nComplete.raw('text082591a16602'), value: cpu, onChange: setCpu, min: 1, max: 32 }), _jsx(NumericField, { id: "mem", label: t('memory'), value: memoryGb, onChange: setMemoryGb, min: 1, max: 128 }), _jsx(NumericField, { id: "disk", label: t('disk'), value: diskGb, onChange: setDiskGb, min: 1, max: 500 })] })] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: "tpl-entry", children: [t('entrypoint'), " ", _jsxs("span", { className: "text-muted-foreground", children: ["(", t('optional'), ")"] })] }), _jsx(Textarea, { id: "tpl-entry", placeholder: t('entrypointPlaceholder'), value: entrypoint, onChange: (e) => setEntrypoint(e.target.value), className: "font-mono text-xs", rows: 2 })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: submitting, children: t('cancel') }), _jsxs(Button, { onClick: () => (isEdit ? editMut.mutate() : createMut.mutate()), disabled: !canSubmit || submitting, children: [submitting && _jsx(Loading, { className: "mr-2 size-4" }), isEdit ? t('save') : t('create')] })] })] }) }));
158
+ }, disabled: isEdit, "aria-invalid": !!slugError }), slugError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: slugError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('imageSource') }), _jsxs("div", { className: "mt-2 grid grid-cols-2 gap-2", children: [_jsxs("button", { type: "button", onClick: () => setMode('image'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'image' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Package, { className: "size-4" }), _jsx("span", { className: "font-medium", children: t('publicImage') })] }), _jsxs("span", { className: "text-muted-foreground text-xs", children: [t('example'), " ", _jsx("code", { className: "font-mono", children: "python:3.12-slim" })] })] }), _jsxs("button", { type: "button", onClick: () => setMode('dockerfile'), className: cn('border-border/60 flex flex-col items-start gap-1 rounded-md border p-3 text-left text-sm transition-colors', mode === 'dockerfile' && 'border-foreground/30 bg-muted/40'), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FileCode, { className: "size-4" }), _jsx("span", { className: "font-medium", children: tI18nComplete.raw('textdd2c0eb6ea5c') })] }), _jsx("span", { className: "text-muted-foreground text-xs", children: t('pathInsideRepo') })] })] }), _jsxs("div", { className: "mt-3", children: [mode === 'image' ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-image", children: t('image') }), _jsx(Input, { id: "tpl-image", placeholder: tI18nComplete.raw('text1d0447543fac'), value: image, onChange: (e) => setImage(e.target.value) }), _jsxs("p", { className: "text-muted-foreground mt-1 text-xs", children: [t('specificTag'), " (", _jsx("code", { className: "font-mono", children: "latest" }), ")."] })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "tpl-df", children: t('dockerfilePath') }), _jsx(Input, { id: "tpl-df", placeholder: tI18nComplete.raw('text1bc7a47dada1'), value: dockerfilePath, onChange: (e) => setDockerfilePath(e.target.value) }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: t('relativeToRoot') })] })), sourceError && _jsx("p", { className: "text-destructive mt-1 text-xs", children: sourceError })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('resources') }), _jsxs("div", { className: "mt-2 grid grid-cols-3 gap-3", children: [_jsx(NumericField, { id: "cpu", label: tI18nComplete.raw('text082591a16602'), value: cpu, onChange: setCpu, min: 1, max: 32 }), _jsx(NumericField, { id: "mem", label: t('memory'), value: memoryGb, onChange: setMemoryGb, min: 1, max: 128 }), _jsx(NumericField, { id: "disk", label: t('disk'), value: diskGb, onChange: setDiskGb, min: 1, max: 500 })] })] }), _jsxs("div", { children: [_jsxs(Label, { htmlFor: "tpl-entry", children: [t('entrypoint'), " ", _jsxs("span", { className: "text-muted-foreground", children: ["(", t('optional'), ")"] })] }), _jsx(Textarea, { id: "tpl-entry", placeholder: t('entrypointPlaceholder'), value: entrypoint, onChange: (e) => setEntrypoint(e.target.value), className: "font-mono text-xs", rows: 2 })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(Checkbox, { id: "tpl-residential-ip", className: "mt-0.5", checked: residentialProxy, onCheckedChange: (checked) => setResidentialProxy(checked === true) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "tpl-residential-ip", children: t('residentialIp') }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: t('residentialIpHint') })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: submitting, children: t('cancel') }), _jsxs(Button, { onClick: () => (isEdit ? editMut.mutate() : createMut.mutate()), disabled: !canSubmit || submitting, children: [submitting && _jsx(Loading, { className: "mr-2 size-4" }), isEdit ? t('save') : t('create')] })] })] }) }));
152
159
  }
153
160
  function NumericField({ id, label, value, onChange, min, max, }) {
154
161
  return (_jsxs("div", { children: [_jsx(Label, { htmlFor: id, className: "text-xs", children: label }), _jsx(Input, { id: id, type: "number", min: min, max: max, value: value, onChange: (e) => onChange(e.target.value) })] }));
@@ -30,7 +30,7 @@ export interface ShowContentProps {
30
30
  type: string;
31
31
  title?: string;
32
32
  description?: string;
33
- path?: string;
33
+ path?: string | null;
34
34
  url?: string;
35
35
  content?: string;
36
36
  language?: string;
@@ -80,7 +80,7 @@ export interface ShowContentProps {
80
80
  */
81
81
  toolbarActions?: React.ReactNode;
82
82
  }
83
- export declare function ShowContentRenderer({ type, title, description, path, url, content, language, aspectRatio, attachment, sha256, LocalhostPreview, fill, onStatusChange, toolbarActions, }: ShowContentProps): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
83
+ export declare function ShowContentRenderer({ type, title, description, path: inputPath, url, content, language, aspectRatio, attachment, sha256, LocalhostPreview, fill, onStatusChange, toolbarActions, }: ShowContentProps): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
84
84
  export interface ShowCarouselItem {
85
85
  type: string;
86
86
  title?: string;
@@ -172,7 +172,8 @@ function FileCard({ title, fileName, path }) {
172
172
  return (_jsxs("div", { className: "flex items-center gap-4 px-5 py-5", children: [_jsx("div", { className: "bg-muted/20 flex size-12 items-center justify-center rounded-xl", children: _jsx(FileText, { className: "text-muted-foreground/40 size-6" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-foreground truncate text-sm font-medium", children: title || fileName }), _jsx("div", { className: "text-muted-foreground/50 mt-0.5 truncate font-mono text-xs", children: path })] })] }));
173
173
  }
174
174
  // ── Component ──────────────────────────────────────────────────────────────
175
- export function ShowContentRenderer({ type, title = '', description = '', path = '', url = '', content = '', language = '', aspectRatio = '', attachment, sha256, LocalhostPreview, fill = false, onStatusChange, toolbarActions, }) {
175
+ export function ShowContentRenderer({ type, title = '', description = '', path: inputPath, url = '', content = '', language = '', aspectRatio = '', attachment, sha256, LocalhostPreview, fill = false, onStatusChange, toolbarActions, }) {
176
+ const path = inputPath ?? '';
176
177
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
177
178
  const tImageGen = useTranslations('imageGen');
178
179
  const arCSS = showAspectRatioToCSS(aspectRatio);
package/dist/i18n/en.d.ts CHANGED
@@ -577,6 +577,8 @@ declare const catalog: {
577
577
  entrypoint: string;
578
578
  optional: string;
579
579
  entrypointPlaceholder: string;
580
+ residentialIp: string;
581
+ residentialIpHint: string;
580
582
  cancel: string;
581
583
  save: string;
582
584
  create: string;
@@ -1930,6 +1932,7 @@ declare const catalog: {
1930
1932
  capture_device_down: string;
1931
1933
  capture_device_recovered: string;
1932
1934
  message: string;
1935
+ artifact_comment: string;
1933
1936
  };
1934
1937
  kindDescription: {
1935
1938
  turn_done: string;
@@ -1942,6 +1945,7 @@ declare const catalog: {
1942
1945
  capture_device_down: string;
1943
1946
  capture_device_recovered: string;
1944
1947
  message: string;
1948
+ artifact_comment: string;
1945
1949
  };
1946
1950
  bell: {
1947
1951
  label: string;
@@ -1966,6 +1970,7 @@ declare const catalog: {
1966
1970
  openReminders: string;
1967
1971
  openTriggers: string;
1968
1972
  openConversation: string;
1973
+ openArtifact: string;
1969
1974
  };
1970
1975
  session: {
1971
1976
  mute: string;
@@ -4873,6 +4878,7 @@ declare const catalog: {
4873
4878
  textce1e609b7bf5: string;
4874
4879
  textce34af36d804: string;
4875
4880
  textce4683e7013a: string;
4881
+ text68971283841a: string;
4876
4882
  textce49e021df99: string;
4877
4883
  textce4c96225f63: string;
4878
4884
  textce5e9df4a78f: string;
@@ -8276,5 +8282,17 @@ declare const catalog: {
8276
8282
  copyFailed: string;
8277
8283
  };
8278
8284
  };
8285
+ sessionDesktop: {
8286
+ title: string;
8287
+ openTab: string;
8288
+ offline: string;
8289
+ start: string;
8290
+ retry: string;
8291
+ takeOver: string;
8292
+ userControl: string;
8293
+ agentControl: string;
8294
+ returnControl: string;
8295
+ escape: string;
8296
+ };
8279
8297
  };
8280
8298
  export default catalog;
package/dist/i18n/en.js CHANGED
@@ -578,6 +578,8 @@ const catalog = {
578
578
  "entrypoint": "Entrypoint",
579
579
  "optional": "optional",
580
580
  "entrypointPlaceholder": "Leave blank to use the Kortix default (recommended).",
581
+ "residentialIp": "Residential IP",
582
+ "residentialIpHint": "Send this sandbox's web traffic through a residential IP address, billed per GB from your credits. Sessions start only where the deployment offers it.",
581
583
  "cancel": "Cancel",
582
584
  "save": "Save changes",
583
585
  "create": "Create template",
@@ -1676,8 +1678,8 @@ const catalog = {
1676
1678
  "sending": "Sending..."
1677
1679
  },
1678
1680
  "unified": {
1679
- "welcome": "Welcome to Kortix",
1680
- "tagline": "The open-source AI Operating System",
1681
+ "welcome": "Welcome to Kortix CompanyOS",
1682
+ "tagline": "The open-source CompanyOS",
1681
1683
  "email": "Email",
1682
1684
  "emailPlaceholder": "Your email address",
1683
1685
  "password": "Password",
@@ -1930,7 +1932,8 @@ const catalog = {
1930
1932
  "automation_recovered": "Recovery alert",
1931
1933
  "capture_device_down": "Capture computer stopped",
1932
1934
  "capture_device_recovered": "Capture computer recording again",
1933
- "message": "Message"
1935
+ "message": "Message",
1936
+ "artifact_comment": "Artifact comments"
1934
1937
  },
1935
1938
  "kindDescription": {
1936
1939
  "turn_done": "A turn ends.",
@@ -1942,7 +1945,8 @@ const catalog = {
1942
1945
  "automation_recovered": "A failing trigger or reminder works again.",
1943
1946
  "capture_device_down": "A Kortix Capture computer is not recording. Sent to people who manage Capture.",
1944
1947
  "capture_device_recovered": "A Kortix Capture computer that stopped records again.",
1945
- "message": "Someone @-mentions you, or replies in a thread you posted in, in a room."
1948
+ "message": "Someone @-mentions you, or replies in a thread you posted in, in a room.",
1949
+ "artifact_comment": "Feedback, mentions, and replies on Artifacts you can view."
1946
1950
  },
1947
1951
  "bell": {
1948
1952
  "label": "Notifications",
@@ -1966,7 +1970,8 @@ const catalog = {
1966
1970
  "openSession": "Open session",
1967
1971
  "openReminders": "Open reminders",
1968
1972
  "openTriggers": "Open triggers",
1969
- "openConversation": "Open conversation"
1973
+ "openConversation": "Open conversation",
1974
+ "openArtifact": "Open Artifact"
1970
1975
  },
1971
1976
  "session": {
1972
1977
  "mute": "Mute notifications",
@@ -2720,7 +2725,7 @@ const catalog = {
2720
2725
  "text18885f27b5af": "Python",
2721
2726
  "text1898b198a7e6": "Nobody present",
2722
2727
  "text18a4e711991b": "Enriches leads from the CRM, researches each account and drafts tailored outreach.",
2723
- "text18f31a325716": "About Kortix – The open-source AI Operating System",
2728
+ "text18f31a325716": "About Kortix CompanyOS – the open-source operating system for your company",
2724
2729
  "text18fb4a5618b9": "The same parameter view used for live approvals.",
2725
2730
  "text18fdd549b2ed": "New",
2726
2731
  "text192173c3beda": "billed to your own provider keys",
@@ -2760,7 +2765,7 @@ const catalog = {
2760
2765
  "text1d55ff7c387c": "View options",
2761
2766
  "text1d59a5e09714": "Approval rules",
2762
2767
  "text1d6d3a460fb1": "DOCX document",
2763
- "text1d7a4d1f09bb": "We are building the open-source AI Operating System. Every company should own all of it.",
2768
+ "text1d7a4d1f09bb": "We are building the open-source CompanyOS. Every company should own all of it.",
2764
2769
  "text1d826553e961": "Review changes proposed by your agents",
2765
2770
  "text1d96669797aa": "sentBody",
2766
2771
  "text1de222b90941": "Add another project",
@@ -2926,7 +2931,7 @@ const catalog = {
2926
2931
  "text2bcb43cbc8f6": "view",
2927
2932
  "text2bd873d6c734": "Breadcrumb",
2928
2933
  "text2bdb956493fd": "Read the webhook handler",
2929
- "text2bf70270bfde": "Open-source AI Operating System: your agents, skills, memory, and connectors in one repo you own. Any model. Self-host or cloud.",
2934
+ "text2bf70270bfde": "Open-source CompanyOS: your agents, skills, memory, and connectors in one repo you own. Any model. Self-host or cloud.",
2930
2935
  "text2bfc3471571e": "Approvals",
2931
2936
  "text2c014f8f8986": "Cursor",
2932
2937
  "text2c063e7ab5f6": "The recipe for the machine a session runs on, and the record of every time Kortix prepared one.",
@@ -3213,7 +3218,7 @@ const catalog = {
3213
3218
  "text44160e26b787": "Not connected yet — the agent and your triggers cannot call this connector.",
3214
3219
  "text442e47369852": "List Kortix public documentation and marketing pages.",
3215
3220
  "text44368adad746": "Postgres",
3216
- "text445cf0a2abfa": "Kortix is building the open-source AI Operating System. A company owns all of it — every agent, all of their data, every skill, every connector, the memory, the whole configuration, on their own infrastructure.",
3221
+ "text445cf0a2abfa": "Kortix is building the open-source CompanyOS. A company owns all of it — every agent, all of their data, every skill, every connector, the memory, the whole configuration, on their own infrastructure.",
3217
3222
  "text446f4eabf99f": "What this agent is for",
3218
3223
  "text44dedd0f97fe": "New file",
3219
3224
  "text44dfa6d7ebf7": "Could not revoke",
@@ -4872,8 +4877,9 @@ const catalog = {
4872
4877
  "textce0faee27592": "billing payment credit card subscription manage wallet tier plan limits overview spend",
4873
4878
  "textce18e358bf01": "Try a different term.",
4874
4879
  "textce1e609b7bf5": "Sandbox ports only",
4875
- "textce34af36d804": "Kortix – The open-source AI Operating System",
4880
+ "textce34af36d804": "Kortix CompanyOS – the open-source operating system for your company",
4876
4881
  "textce4683e7013a": "Channel",
4882
+ "text68971283841a": "Subject",
4877
4883
  "textce49e021df99": "The session owner or a project manager can reload it.",
4878
4884
  "textce4c96225f63": "1 attempt",
4879
4885
  "textce5e9df4a78f": "Service account",
@@ -5635,7 +5641,7 @@ const catalog = {
5635
5641
  "text2bbd1ec2197d": "Connected app",
5636
5642
  "textda3d3fb8f598": "Agent session",
5637
5643
  "text4dcb7e3b10a9": "SCIM token",
5638
- "text5f58426c214e": "We are building the open-source AI Operating System. Every company should own all of it.",
5644
+ "text5f58426c214e": "We are building the open-source CompanyOS. Every company should own all of it.",
5639
5645
  "text80043b03898d": "Update the Kortix app in Teams",
5640
5646
  "textdf757dbe33eb": "Your Teams catalog has version {value0} of the Kortix app. Version {value1} has every command and permission your agent uses in Teams. A Teams admin publishes the update. Then a team owner updates the app in each team where Teams offers it: the team → ⋯ → Manage team → Apps → Update.",
5641
5647
  "textcd2c4b26eedd": "Kortix has no record of the app version in your Teams catalog. Version {value0} has every command and permission your agent uses in Teams. A Teams admin publishes the update. Then a team owner updates the app in each team where Teams offers it: the team → ⋯ → Manage team → Apps → Update.",
@@ -6285,7 +6291,7 @@ const catalog = {
6285
6291
  "createProjectInstead": "Create a new project instead"
6286
6292
  },
6287
6293
  "projectSelector": {
6288
- "welcome": "Welcome to Kortix",
6294
+ "welcome": "Welcome to Kortix CompanyOS",
6289
6295
  "welcomeBack": "Welcome back",
6290
6296
  "descriptionNew": "Create your first project, or join one your team shared.",
6291
6297
  "descriptionReturning": "Open a project, or create a new one.",
@@ -8276,6 +8282,18 @@ const catalog = {
8276
8282
  "connectLabel": "Connect MCP",
8277
8283
  "copyFailed": "Could not copy the command. Try again."
8278
8284
  }
8285
+ },
8286
+ "sessionDesktop": {
8287
+ "title": "Desktop",
8288
+ "openTab": "Open in new tab",
8289
+ "offline": "The desktop is not running on this computer.",
8290
+ "start": "Ask Kortix to run",
8291
+ "retry": "Retry",
8292
+ "takeOver": "Take over",
8293
+ "userControl": "You have control",
8294
+ "agentControl": "Kortix has control",
8295
+ "returnControl": "Return control",
8296
+ "escape": "Esc"
8279
8297
  }
8280
8298
  };
8281
8299
  export default catalog;
@@ -262,7 +262,7 @@ export const REMAINING_UI_TRANSLATION_KEYS = {
262
262
  'Just you': 'textaeed20a96ee3',
263
263
  'Keep model billing with your provider': 'textcd996e5c44e9',
264
264
  'Keys are resolved on our side of the wall. The machine the model drives never holds them.': 'texte6229e214e88',
265
- 'Kortix – The open-source AI Operating System': 'textce34af36d804',
265
+ 'Kortix CompanyOS – the open-source operating system for your company': 'textce34af36d804',
266
266
  'Kortix is building the open AGI platform. A company owns all of it — every agent, all of their data, every skill, every connector, the memory, the whole configuration, on their own infrastructure.': 'text445cf0a2abfa',
267
267
  'Kortix — Presentations': 'textd84b8be91e7b',
268
268
  'Kortix API documentation': 'text7b3c138b383d',
@@ -466,7 +466,7 @@ export const REMAINING_UI_TRANSLATION_KEYS = {
466
466
  'The gate': 'textb46788a7c320',
467
467
  'The in-depth platform walkthrough — the full Kortix surface, from projects and sessions to connectors, channels and change requests.': 'text7484ed6d3897',
468
468
  'The Kortix team': 'text49e7ba9f095d',
469
- 'The open-source AI Operating System for your company. Every agent, skill, and memory is a file in one versioned repo you own — a workforce of AI agents that does real work, shared across your whole team from Slack, Teams, the web, or the CLI. Self-hostable, any model, your keys.': 'text2bf70270bfde',
469
+ 'The open-source CompanyOS. Every agent, skill, and memory is a file in one versioned repo you own — a workforce of AI agents that does real work, shared across your whole team from Slack, Teams, the web, or the CLI. Self-hostable, any model, your keys.': 'text2bf70270bfde',
470
470
  'The questions people ask before the first session.': 'text2b7df1bf48b2',
471
471
  'The record': 'textd9fa05c93112',
472
472
  'This session is mid-turn': 'textc116f723c599',
@@ -814,7 +814,7 @@ export const REMAINING_UI_TRANSLATION_KEYS = {
814
814
  'per session': 'textf92898c93eec',
815
815
  'per message': 'text83d0be53d592',
816
816
  answers: 'text677fe21b65f4',
817
- 'We are building the open-source AI Operating System. Every company should own all of it.': 'text5f58426c214e',
817
+ 'We are building the open-source CompanyOS. Every company should own all of it.': 'text5f58426c214e',
818
818
  'One kortix.yaml.': 'text52867b778586',
819
819
  'Your AI workforce, as code.': 'textd0dfd435b2f4',
820
820
  'Agents, skills, tools, connectors and schedules are files in a repo. Edit them in your IDE, ship them with one command.': 'text6dc0b331852f',
@@ -90,8 +90,8 @@ const originals = new WeakMap();
90
90
  * trade-off (host-based tenancy would be the way to remove it).
91
91
  *
92
92
  * Title: Next writes `<title>` on every navigation from the route's metadata:
93
- * the site default (`Kortix – The open-source AI Operating System`) on
94
- * routes with no title of their own, `<page> | Kortix` elsewhere. A
93
+ * the site default (`Kortix CompanyOS – the open-source operating system for your company`) on
94
+ * routes with no title of their own, `<page> | Kortix CompanyOS` elsewhere. A
95
95
  * `MutationObserver` on `<head>` rewrites whatever Next just wrote, so the
96
96
  * swap survives navigation without touching every page's metadata: the site
97
97
  * default collapses to the org name alone (its tagline is Kortix's, not
@@ -190,7 +190,7 @@ function BrandingDocumentEffect({ branding }) {
190
190
  const current = document.title;
191
191
  if (!current.includes('Kortix'))
192
192
  return;
193
- const next = current === siteMetadata.title ? appName : current.replace(/\bKortix\b/g, appName);
193
+ const next = current === siteMetadata.title ? appName : current.replace(/\bKortix(?: CompanyOS)?\b/g, appName);
194
194
  if (next !== current)
195
195
  document.title = next;
196
196
  };
@@ -1,9 +1,7 @@
1
1
  /** @internal Kortix apps only. No semver: it may change in any release. */
2
2
  /**
3
- * The `notification_center` project flag on the web (KRTX-1742). It is off by
4
- * default. A project with the flag off behaves as before the notification
5
- * center: no bell, no inbox request, no Web Push subscription, no mute item,
6
- * and the four per-browser kind switches in Settings > Notifications.
3
+ * The strict `notification_center` gate controls session mute/watch actions.
4
+ * Inbox surfaces also work when artifacts and artifact_comments are both on.
7
5
  *
8
6
  * - With a project id: `useFeatureFlag`, fail-closed, on the shared
9
7
  * `qk.project.detail(id)` entry that the project shell reads.
@@ -17,22 +15,28 @@
17
15
  import type { KortixProject } from '@kortix/sdk';
18
16
  import { type QueryClient } from '@tanstack/react-query';
19
17
  export declare const NOTIFICATION_CENTER_FLAG = "notification_center";
20
- type FlagHolder = Pick<KortixProject, 'experimental'> | null | undefined;
18
+ type FlagHolder = {
19
+ experimental?: Partial<NonNullable<KortixProject['experimental']>>;
20
+ } | null | undefined;
21
21
  /** The predicate of `useFeatureFlag`: the server said exactly `true`. */
22
22
  export declare function notificationCenterOn(project: FlagHolder): boolean;
23
+ export declare function notificationInboxOn(project: FlagHolder): boolean;
23
24
  /**
24
25
  * The flag for one project from the query cache: its detail, else a cached
25
26
  * project list that holds it. Undefined when neither holds it. Sends no request.
26
27
  */
27
28
  export declare function cachedNotificationCenterAnswer(client: QueryClient | null, projectId: string | null | undefined): boolean | undefined;
29
+ export declare function cachedNotificationInboxAnswer(client: QueryClient | null, projectId: string | null | undefined): boolean | undefined;
28
30
  /** The flag for one project from the query cache. False when nothing cached holds it. */
29
31
  export declare function cachedNotificationCenter(client: QueryClient | null, projectId: string | null | undefined): boolean;
30
32
  /** True when any project in the query cache has the flag on. */
31
33
  export declare function anyCachedNotificationCenter(client: QueryClient): boolean;
34
+ export declare function anyCachedNotificationInbox(client: QueryClient): boolean;
32
35
  /** Is the notification center on for `projectId`, or, without one, for any cached project. */
33
36
  export declare function useNotificationCenter(projectId?: string | null): boolean;
37
+ export declare function useNotificationInboxEnabled(projectId?: string | null): boolean;
34
38
  /**
35
- * The gate of `NotificationHost`: `useNotificationCenter`, except while the
39
+ * The inbox gate of `NotificationHost`, except while the
36
40
  * project's detail loads. Then a cached project list answers, else the
37
41
  * previous answer stays. So entering a flag-on project from `/projects` or
38
42
  * from another flag-on project does not unmount the host and subscribe Web
@@ -7,25 +7,34 @@ export const NOTIFICATION_CENTER_FLAG = 'notification_center';
7
7
  export function notificationCenterOn(project) {
8
8
  return project?.experimental?.[NOTIFICATION_CENTER_FLAG] === true;
9
9
  }
10
+ export function notificationInboxOn(project) {
11
+ return notificationCenterOn(project) || (project?.experimental?.artifacts === true && project.experimental.artifact_comments === true);
12
+ }
10
13
  /**
11
14
  * The flag for one project from the query cache: its detail, else a cached
12
15
  * project list that holds it. Undefined when neither holds it. Sends no request.
13
16
  */
14
17
  export function cachedNotificationCenterAnswer(client, projectId) {
18
+ return cachedFlagAnswer(client, projectId, notificationCenterOn);
19
+ }
20
+ function cachedFlagAnswer(client, projectId, enabled) {
15
21
  if (!client || !projectId)
16
22
  return undefined;
17
23
  const detail = client.getQueryData(qk.project.detail(projectId));
18
24
  if (detail?.project)
19
- return notificationCenterOn(detail.project);
25
+ return enabled(detail.project);
20
26
  for (const [, data] of client.getQueriesData({ queryKey: qk.projects.scope() })) {
21
27
  if (!Array.isArray(data))
22
28
  continue;
23
29
  const project = data.find((row) => row?.project_id === projectId);
24
30
  if (project)
25
- return notificationCenterOn(project);
31
+ return enabled(project);
26
32
  }
27
33
  return undefined;
28
34
  }
35
+ export function cachedNotificationInboxAnswer(client, projectId) {
36
+ return cachedFlagAnswer(client, projectId, notificationInboxOn);
37
+ }
29
38
  /** The flag for one project from the query cache. False when nothing cached holds it. */
30
39
  export function cachedNotificationCenter(client, projectId) {
31
40
  return cachedNotificationCenterAnswer(client, projectId) === true;
@@ -33,21 +42,24 @@ export function cachedNotificationCenter(client, projectId) {
33
42
  const [ROOT, LIST_SCOPE] = qk.projects.scope();
34
43
  const [, DETAIL_SCOPE, , DETAIL] = qk.project.detail('');
35
44
  /** A project-list entry or a project-detail entry that holds a project with the flag on. */
36
- function holdsFlagOn(query) {
45
+ function holdsFlagOn(query, enabled) {
37
46
  const key = query.queryKey;
38
47
  const data = query.state.data;
39
48
  if (key[0] !== ROOT)
40
49
  return false;
41
50
  if (key[1] === LIST_SCOPE)
42
- return Array.isArray(data) && data.some(notificationCenterOn);
51
+ return Array.isArray(data) && data.some(enabled);
43
52
  if (key[1] === DETAIL_SCOPE && key.length === 4 && key[3] === DETAIL) {
44
- return notificationCenterOn(data?.project);
53
+ return enabled(data?.project);
45
54
  }
46
55
  return false;
47
56
  }
48
57
  /** True when any project in the query cache has the flag on. */
49
58
  export function anyCachedNotificationCenter(client) {
50
- return client.getQueryCache().getAll().some(holdsFlagOn);
59
+ return client.getQueryCache().getAll().some((query) => holdsFlagOn(query, notificationCenterOn));
60
+ }
61
+ export function anyCachedNotificationInbox(client) {
62
+ return client.getQueryCache().getAll().some((query) => holdsFlagOn(query, notificationInboxOn));
51
63
  }
52
64
  /**
53
65
  * A `useSyncExternalStore` subscription to the query cache, live only while
@@ -76,14 +88,24 @@ function useCacheSubscription(client, active) {
76
88
  function useAnyCachedNotificationCenter(client, scan) {
77
89
  return useSyncExternalStore(useCacheSubscription(client, scan), () => scan && anyCachedNotificationCenter(client), () => false);
78
90
  }
91
+ function useAnyCachedNotificationInbox(client, scan) {
92
+ return useSyncExternalStore(useCacheSubscription(client, scan), () => scan && anyCachedNotificationInbox(client), () => false);
93
+ }
79
94
  /** Is the notification center on for `projectId`, or, without one, for any cached project. */
80
95
  export function useNotificationCenter(projectId) {
81
96
  const project = useFeatureFlag(projectId, NOTIFICATION_CENTER_FLAG).enabled;
82
97
  const anyProject = useAnyCachedNotificationCenter(useQueryClient(), !projectId);
83
98
  return projectId ? project : anyProject;
84
99
  }
100
+ export function useNotificationInboxEnabled(projectId) {
101
+ const center = useFeatureFlag(projectId, NOTIFICATION_CENTER_FLAG).enabled;
102
+ const artifacts = useFeatureFlag(projectId, 'artifacts').enabled;
103
+ const comments = useFeatureFlag(projectId, 'artifact_comments').enabled;
104
+ const anyProject = useAnyCachedNotificationInbox(useQueryClient(), !projectId);
105
+ return projectId ? center || (artifacts && comments) : anyProject;
106
+ }
85
107
  /**
86
- * The gate of `NotificationHost`: `useNotificationCenter`, except while the
108
+ * The inbox gate of `NotificationHost`, except while the
87
109
  * project's detail loads. Then a cached project list answers, else the
88
110
  * previous answer stays. So entering a flag-on project from `/projects` or
89
111
  * from another flag-on project does not unmount the host and subscribe Web
@@ -100,14 +122,14 @@ export function useNotificationHostGate(projectId) {
100
122
  const detail = useSyncExternalStore(useCacheSubscription(client, !!projectId), () => (projectId ? client.getQueryState(qk.project.detail(projectId)) : undefined), () => undefined);
101
123
  const flag = {
102
124
  isLoading: !detail || detail.status === 'pending',
103
- enabled: notificationCenterOn(detail?.data?.project),
125
+ enabled: notificationInboxOn(detail?.data?.project),
104
126
  };
105
- const anyProject = useAnyCachedNotificationCenter(client, !projectId);
127
+ const anyProject = useAnyCachedNotificationInbox(client, !projectId);
106
128
  const [previous, setPrevious] = useState(false);
107
129
  let answer = anyProject;
108
130
  if (projectId) {
109
131
  answer = flag.isLoading
110
- ? (cachedNotificationCenterAnswer(client, projectId) ?? previous)
132
+ ? (cachedNotificationInboxAnswer(client, projectId) ?? previous)
111
133
  : flag.enabled;
112
134
  }
113
135
  // The answer from an earlier render: React's derived-state pattern.
@@ -24,6 +24,9 @@ const EXTENSION_PROTOCOL_PREFIXES = [
24
24
  ];
25
25
  const INJECTED_APP_SOURCE_PATTERNS = [
26
26
  /^app:\/\/\/scripts\/inpage\.js$/,
27
+ // Injected extension content script; KRTX-3704's onMessage TypeError has
28
+ // only this source, never a first-party _next bundle or resolved source.
29
+ /^app:\/\/\/js\/content\.js$/,
27
30
  // CookieYes (https://www.cookieyes.com) cookie-consent SDK runtime, served
28
31
  // from its own CDN workspace directory
29
32
  // `https://cdn-cookieyes.com/client_data/<workspace-id>/…`: `script.js` is
@@ -8,9 +8,9 @@
8
8
  */
9
9
  export const CANONICAL_ORIGIN = 'https://kortix.com';
10
10
  export const siteMetadata = {
11
- name: 'Kortix',
12
- title: 'Kortix – The open-source AI Operating System',
13
- description: 'Open-source AI Operating System: your agents, skills, memory, and connectors in one repo you own. Any model. Self-host or cloud.',
11
+ name: 'Kortix CompanyOS',
12
+ title: 'Kortix CompanyOS – the open-source operating system for your company',
13
+ description: 'Open-source CompanyOS: your agents, skills, memory, and connectors in one repo you own. Any model. Self-host or cloud.',
14
14
  url: CANONICAL_ORIGIN,
15
- keywords: 'Kortix, AI Operating System, open-source AI Operating System, AI OS, open-source AI OS, AI Operating System for companies, what is an AI Operating System, open-source alternative to Claude Cowork, ChatGPT Work alternative, company as a git repo, agents skills and memory as files, shared AI agents, scoped access, self-hosted AI agents, connect 3000 tools, agent orchestration, AI-native company, AI operations',
15
+ keywords: 'Kortix, Kortix CompanyOS, CompanyOS, open-source CompanyOS, company operating system, what is a CompanyOS, open-source alternative to Claude Cowork, ChatGPT Work alternative, company as a git repo, agents skills and memory as files, shared AI agents, scoped access, self-hosted AI agents, connect 3000 tools, agent orchestration, AI-native company, AI operations',
16
16
  };