@kortix/ui 0.14.9 → 0.14.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/_vendor/kortix-shared.js +55 -2
  2. package/dist/_vendor/shared/artifact-comments.d.ts +235 -0
  3. package/dist/_vendor/shared/channel-descriptors.d.ts +96 -0
  4. package/dist/_vendor/shared/channel-message.d.ts +13 -2
  5. package/dist/_vendor/shared/index.d.ts +1 -0
  6. package/dist/_vendor/shared/runtime-versions.d.ts +9 -0
  7. package/dist/apps/app-preview-pool.js +19 -2
  8. package/dist/artifacts/artifact-frame.js +7 -3
  9. package/dist/customize/sections/component/channel-access-panel.js +4 -4
  10. package/dist/customize/sections/component/telegram-channel-row.js +7 -5
  11. package/dist/customize/sections/view/agent-editor-catalog.js +5 -0
  12. package/dist/customize/sections/view/channels-view.d.ts +2 -1
  13. package/dist/customize/sections/view/channels-view.js +12 -7
  14. package/dist/customize/settings/rail.js +9 -0
  15. package/dist/customize/settings/settings-tabs.d.ts +1 -1
  16. package/dist/customize/settings/settings-tabs.js +1 -0
  17. package/dist/customize/settings/tabs/chrome-connections-tab.d.ts +6 -0
  18. package/dist/customize/settings/tabs/chrome-connections-tab.js +99 -0
  19. package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
  20. package/dist/customize/shared/catalog/catalog-empty-state.d.ts +3 -3
  21. package/dist/customize/shared/catalog/catalog-empty-state.js +6 -4
  22. package/dist/files/renderers/image-frame.d.ts +3 -1
  23. package/dist/files/renderers/image-frame.js +2 -2
  24. package/dist/files/renderers/show-content-renderer.d.ts +2 -2
  25. package/dist/files/renderers/show-content-renderer.js +2 -1
  26. package/dist/i18n/en.d.ts +206 -0
  27. package/dist/i18n/en.js +219 -13
  28. package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
  29. package/dist/internal/apps-and-extensions/apps-modal.d.ts +9 -0
  30. package/dist/internal/apps-and-extensions/apps-modal.js +107 -0
  31. package/dist/internal/branding/branding-provider.js +3 -3
  32. package/dist/internal/chrome/browser-icon.d.ts +24 -0
  33. package/dist/internal/chrome/browser-icon.js +58 -0
  34. package/dist/internal/chrome/chrome-card-state.d.ts +16 -0
  35. package/dist/internal/chrome/chrome-card-state.js +9 -0
  36. package/dist/internal/chrome/chrome-connect.d.ts +69 -0
  37. package/dist/internal/chrome/chrome-connect.js +178 -0
  38. package/dist/internal/chrome/chrome-mock.d.ts +4 -0
  39. package/dist/internal/chrome/chrome-mock.js +15 -0
  40. package/dist/internal/chrome/chrome-names.d.ts +15 -0
  41. package/dist/internal/chrome/chrome-names.js +35 -0
  42. package/dist/internal/chrome/extension.d.ts +47 -0
  43. package/dist/internal/chrome/extension.js +91 -0
  44. package/dist/internal/chrome/use-chrome-offered.d.ts +11 -0
  45. package/dist/internal/chrome/use-chrome-offered.js +22 -0
  46. package/dist/internal/connector-logo/connector-logo-source.d.ts +2 -1
  47. package/dist/internal/connector-logo/connector-logo-source.js +7 -6
  48. package/dist/internal/hooks/use-notification-center.d.ts +10 -6
  49. package/dist/internal/hooks/use-notification-center.js +32 -10
  50. package/dist/internal/icons/edge.d.ts +5 -0
  51. package/dist/internal/icons/edge.js +10 -0
  52. package/dist/internal/lib/browser-noise/rules/extensions.js +17 -3
  53. package/dist/internal/lib/detect-os.d.ts +43 -0
  54. package/dist/internal/lib/detect-os.js +70 -0
  55. package/dist/internal/lib/site-metadata.js +4 -4
  56. package/dist/internal/lib/use-project-feature-flags.js +5 -1
  57. package/dist/internal/notifications/notification-bell.js +4 -3
  58. package/dist/internal/notifications/notification-rows.d.ts +1 -1
  59. package/dist/internal/notifications/notification-rows.js +2 -0
  60. package/dist/internal/stores/apps-modal-store.d.ts +8 -0
  61. package/dist/internal/stores/apps-modal-store.js +6 -0
  62. package/dist/internal/stores/composer-prefill-store.d.ts +12 -5
  63. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  64. package/dist/markdown/setup-links/approval-decision.js +3 -1
  65. package/dist/primitives/empty-state.d.ts +2 -2
  66. package/dist/primitives/empty-state.js +6 -5
  67. package/dist/primitives/mermaid-renderer.js +1 -1
  68. package/dist/session/composer/send-stop-control.js +8 -3
  69. package/dist/session/header/session-site-header.js +3 -1
  70. package/dist/session/panel/easy/detail-view.js +4 -0
  71. package/dist/session/panel/mobile-tool-drawer.js +5 -3
  72. package/dist/session/panel/open-session-quick-view.js +2 -2
  73. package/dist/session/panel/panel-fallback.js +10 -7
  74. package/dist/session/panel/session-audit-shared.js +3 -1
  75. package/dist/session/panel/session-desktop.d.ts +6 -0
  76. package/dist/session/panel/session-desktop.js +74 -0
  77. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  78. package/dist/session/panel/session-panel-provider.js +17 -1
  79. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  80. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  81. package/dist/session/panel/use-session-panel-layout.js +8 -0
  82. package/dist/session/participants/session-participants.d.ts +3 -1
  83. package/dist/session/participants/session-participants.js +2 -2
  84. package/dist/session/prompts/approval-request.js +2 -0
  85. package/dist/session/status/session-tab-title.d.ts +1 -1
  86. package/dist/session/status/session-tab-title.js +1 -1
  87. package/dist/session/thread/session-chat.js +12 -0
  88. package/dist/session/thread/session-context-sub-sessions.js +1 -1
  89. package/dist/session/thread/session-thread.d.ts +13 -1
  90. package/dist/session/thread/session-thread.js +15 -5
  91. package/dist/session/thread/session-transcript-class.d.ts +5 -1
  92. package/dist/session/thread/session-transcript-class.js +5 -1
  93. package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
  94. package/dist/session/tool/tools/image-gen-tool.js +21 -11
  95. package/dist/session/turn/user-message.d.ts +7 -5
  96. package/dist/session/turn/user-message.js +14 -8
  97. package/dist/styles.css +1 -1
  98. package/dist/workspace/session-workspace.js +24 -5
  99. package/dist/workspace/sessions/session-label-badges.js +2 -1
  100. package/dist/workspace/sidebar/footer/project-chrome-nav.d.ts +10 -0
  101. package/dist/workspace/sidebar/footer/project-chrome-nav.js +56 -0
  102. package/dist/workspace/sidebar/project-sidebar.js +5 -1
  103. package/package.json +43 -3
  104. package/skills/kortix-ui/references/modules.md +1 -1
@@ -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 = 'slack' | 'teams' | 'email' | 'telegram' | 'computer';
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 = ['slack', 'teams', 'email', 'telegram', 'computer'];
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 match = /^(?:native:|kortix_)(slack|teams|email|telegram|computer)$/.exec(slug);
15
- if (match)
16
- return match[1];
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\/(slack|teams|email|telegram|computer)\.svg$/.exec(source.icon ?? '');
20
- return file ? file[1] : null;
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);
@@ -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.
@@ -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
- * (the wording of the wallet RPC error library). The rejected value is a plain
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
- if (value.code !== -32603 || value.message !== 'Internal JSON-RPC error.')
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;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Platform detection for the /download page and the per-platform redirect
3
+ * routes.
4
+ *
5
+ * Detection runs on the SERVER, from the request's user-agent header, so the
6
+ * page paints the correct filled button (and, on a phone, the mobile card
7
+ * first) on first render. A client-side `useEffect` swap would let a visitor click the wrong
8
+ * installer during the flash — the exact failure this page exists to prevent.
9
+ *
10
+ * Architecture is deliberately NOT detected: macOS ships a single universal
11
+ * .dmg, and macOS user agents misreport arch anyway (they always claim Intel).
12
+ */
13
+ /** Fixed row order. Detection never reorders rows; it only picks the filled button. */
14
+ export const DESKTOP_ORDER = ['macos', 'windows', 'linux'];
15
+ export const MOBILE_ORDER = ['ios', 'android'];
16
+ const PLATFORM_ALIASES = {
17
+ mac: 'macos',
18
+ macos: 'macos',
19
+ osx: 'macos',
20
+ darwin: 'macos',
21
+ apple: 'macos',
22
+ win: 'windows',
23
+ windows: 'windows',
24
+ linux: 'linux',
25
+ ios: 'ios',
26
+ iphone: 'ios',
27
+ ipad: 'ios',
28
+ android: 'android',
29
+ };
30
+ /** Map an explicit `?platform=` value or `/download/<segment>` to a platform. */
31
+ export function normalizePlatform(raw) {
32
+ return PLATFORM_ALIASES[(raw || '').toLowerCase()] ?? null;
33
+ }
34
+ /**
35
+ * Best-effort platform from a user-agent string.
36
+ *
37
+ * THE ORDER OF THESE CHECKS IS LOAD-BEARING. Two user agents carry another
38
+ * platform's token:
39
+ * iPad "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)" contains "mac"
40
+ * Android "Mozilla/5.0 (Linux; Android 14; Pixel 8)" contains "linux"
41
+ * So iOS is tested before macOS, and Android before Linux. `detect-os.test.ts`
42
+ * pins both; if you reorder these lines, those tests fail.
43
+ *
44
+ * KNOWN LIMITATION: an iPad with "Request Desktop Website" enabled sends a UA
45
+ * byte-identical to a Mac's. No server can separate them — real detection needs
46
+ * `navigator.maxTouchPoints`, which is not in any request header. That visitor
47
+ * gets the Desktop card first; the Mobile card is still on the page, one tap
48
+ * away. Fixing it client-side would reintroduce the wrong-file-click window
49
+ * this module exists to close.
50
+ *
51
+ * Defaults to macOS: the largest desktop segment for this product, and every
52
+ * other platform stays one click away on the page.
53
+ */
54
+ export function detectPlatform(userAgent) {
55
+ const ua = (userAgent || '').toLowerCase();
56
+ if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod'))
57
+ return 'ios';
58
+ if (ua.includes('android'))
59
+ return 'android';
60
+ if (ua.includes('windows'))
61
+ return 'windows';
62
+ if (ua.includes('mac') || ua.includes('darwin'))
63
+ return 'macos';
64
+ if (ua.includes('linux'))
65
+ return 'linux';
66
+ return 'macos';
67
+ }
68
+ export function isMobilePlatform(platform) {
69
+ return platform === 'ios' || platform === 'android';
70
+ }
@@ -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
  };
@@ -39,9 +39,11 @@ export function useProjectFeatureFlags(projectId) {
39
39
  const agentRooms = useFeatureFlag(projectId, 'agent_rooms');
40
40
  const goals = useFeatureFlag(projectId, 'goals');
41
41
  const artifacts = useFeatureFlag(projectId, 'artifacts');
42
+ const artifactComments = useFeatureFlag(projectId, 'artifact_comments');
42
43
  const emailChannel = useFeatureFlag(projectId, 'email_channel');
43
44
  const workspaceVolume = useFeatureFlag(projectId, 'workspace_volume');
44
45
  const sessionGroups = useFeatureFlag(projectId, 'session_groups');
46
+ const chrome = useFeatureFlag(projectId, 'chrome');
45
47
  return {
46
48
  flags: {
47
49
  marketplace: marketplace.enabled,
@@ -62,11 +64,13 @@ export function useProjectFeatureFlags(projectId) {
62
64
  agent_rooms: agentRooms.enabled,
63
65
  goals: goals.enabled,
64
66
  artifacts: artifacts.enabled,
67
+ artifact_comments: artifactComments.enabled,
65
68
  email_channel: emailChannel.enabled,
66
69
  workspace_volume: workspaceVolume.enabled,
67
70
  session_groups: sessionGroups.enabled,
71
+ chrome: chrome.enabled,
68
72
  },
69
73
  // The trailing hook's loading state — keep this on the LAST hook above.
70
- isLoading: sessionGroups.isLoading,
74
+ isLoading: chrome.isLoading,
71
75
  };
72
76
  }
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  * The bell (KRTX-1742): the person's notification inbox, across projects.
5
5
  *
6
6
  * It sits in the project sidebar header and in `AccountTopBar`, only while the
7
- * `notification_center` flag is on (`useNotificationCenter`). The badge is
7
+ * inbox gate is on (`useNotificationInboxEnabled`). The badge is
8
8
  * the server's unread count, clamped at 99+. A row opens its session (or the
9
9
  * Triggers page for an alert) and marks itself read. When this browser shows
10
10
  * no notifications yet, the panel's footer offers to turn them on.
@@ -33,7 +33,7 @@ import { useNotificationInbox } from '@kortix/sdk/react';
33
33
  import { BellIcon, ChatCircleIcon, QuestionIcon, ShareIcon, ShieldWarningIcon, WarningIcon, XCircleIcon, XIcon, } from '@phosphor-icons/react';
34
34
  import { useState } from 'react';
35
35
  import { badgeCount, tabRows, tabUnreadCount, withoutNotificationParam, } from './notification-rows.js';
36
- import { useNotificationCenter } from '../hooks/use-notification-center.js';
36
+ import { useNotificationInboxEnabled } from '../hooks/use-notification-center.js';
37
37
  import { webPushSupported } from './web-push.js';
38
38
  /** Every kind glyph is solid: Phosphor at `weight="fill"`, the done check is `SolidCheckIcon`. */
39
39
  const solid = (Icon) => function SolidKindIcon(props) {
@@ -51,6 +51,7 @@ const KIND_MARK = {
51
51
  capture_device_down: { icon: solid(WarningIcon), className: 'text-kortix-red' },
52
52
  capture_device_recovered: { icon: SolidCheckIcon, className: 'text-kortix-green' },
53
53
  message: { icon: solid(ChatCircleIcon), className: 'text-muted-foreground' },
54
+ artifact_comment: { icon: solid(ChatCircleIcon), className: 'text-muted-foreground' },
54
55
  };
55
56
  /** Props only: the header, the All / Needs you / Done tabs, the rows, and the footer offer. */
56
57
  export function NotificationPanel({ rows, state, unreadCount, prompt, onOpenRow, onMarkAllRead, onRetry, onTurnOn, onDismissPrompt, }) {
@@ -96,7 +97,7 @@ export function NotificationBell({ projectId, onNavigate, surface = 'bg-backgrou
96
97
  const t = useTranslations('notifications');
97
98
  const viewerId = useUiViewerId();
98
99
  const [open, setOpen] = useState(false);
99
- const notificationCenter = useNotificationCenter(projectId);
100
+ const notificationCenter = useNotificationInboxEnabled(projectId);
100
101
  const inbox = useNotificationInbox({ userId: viewerId, enabled: notificationCenter });
101
102
  const notificationsOn = useWebNotificationStore((s) => s.preferences.enabled);
102
103
  const permission = useWebNotificationStore((s) => s.permission);
@@ -55,7 +55,7 @@ export declare function notificationTag(row: InboxNotification): string;
55
55
  * conversation (a message), its session, the Reminders page (a reminder
56
56
  * alert), or the Triggers page.
57
57
  */
58
- export declare function rowDestination(row: InboxNotification): 'room' | 'session' | 'reminders' | 'triggers';
58
+ export declare function rowDestination(row: InboxNotification): 'artifact' | 'room' | 'session' | 'reminders' | 'triggers';
59
59
  /** The message `public/sw.js` posts to a tab already on a clicked notification's page. */
60
60
  export declare const NOTIFICATION_OPEN_MESSAGE = "kortix:notification-open";
61
61
  /** The notification id a service-worker message says this tab opened, or null. */
@@ -71,6 +71,8 @@ export function notificationTag(row) {
71
71
  * alert), or the Triggers page.
72
72
  */
73
73
  export function rowDestination(row) {
74
+ if (row.artifact_id)
75
+ return 'artifact';
74
76
  if (row.room_id)
75
77
  return 'room';
76
78
  if (row.session_id)
@@ -0,0 +1,8 @@
1
+ /** @internal Kortix apps only. No semver: it may change in any release. */
2
+ /** "Apps and extensions" dialog: any trigger (account menu, sidebar row, `?apps=1`) opens the one mounted in the project sidebar. */
3
+ interface AppsModalState {
4
+ open: boolean;
5
+ setOpen: (open: boolean) => void;
6
+ }
7
+ export declare const useAppsModalStore: import("zustand").UseBoundStore<import("zustand").StoreApi<AppsModalState>>;
8
+ export {};
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+ import { create } from 'zustand';
3
+ export const useAppsModalStore = create((set) => ({
4
+ open: false,
5
+ setOpen: (open) => set({ open }),
6
+ }));