@kortix/ui 0.14.4 → 0.14.5
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/README.md +8 -4
- package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
- package/dist/customize/sections/llm-provider/constants.js +0 -2
- package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
- package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
- package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
- package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
- package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
- package/dist/customize/sections/llm-provider/models-tab.js +1 -10
- package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
- package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
- package/dist/customize/sections/llm-provider/utils.js +7 -14
- package/dist/customize/sections/view/gateway/_shared.js +1 -2
- package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
- package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
- package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
- package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
- package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
- package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
- package/dist/customize/shared/entity/entity-modal.js +7 -4
- package/dist/customize/skills/remove-skill-button.d.ts +16 -0
- package/dist/customize/skills/remove-skill-button.js +42 -0
- package/dist/customize/skills/skills-page.js +1 -1
- package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
- package/dist/customize/triggers/schedule/trigger-list.js +1 -18
- package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
- package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
- package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
- package/dist/files/project/hooks/use-change-requests.js +44 -25
- package/dist/files/renderers/docx/docx-viewer.js +4 -1
- package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
- package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
- package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
- package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
- package/dist/files/viewer/file-content-renderer.d.ts +4 -1
- package/dist/files/viewer/file-content-renderer.js +2 -2
- package/dist/i18n/en.d.ts +26 -35
- package/dist/i18n/en.js +35 -44
- package/dist/index.d.ts +1 -1
- package/dist/internal/hooks/use-notification-center.js +21 -15
- package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
- package/dist/internal/hooks/use-review-session-summary.js +11 -0
- package/dist/internal/lib/browser-error-noise.d.ts +1 -1
- package/dist/internal/lib/browser-error-noise.js +1 -1
- package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
- package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
- package/dist/internal/lib/provider-branding.js +14 -3
- package/dist/internal/lib/trigger-clock.d.ts +42 -0
- package/dist/internal/lib/trigger-clock.js +34 -0
- package/dist/internal/lib/web-notifications.d.ts +6 -0
- package/dist/internal/lib/web-notifications.js +57 -7
- package/dist/internal/notifications/notification-bell.d.ts +13 -0
- package/dist/internal/notifications/notification-bell.js +1 -1
- package/dist/internal/reminders/reminder-filter.d.ts +12 -0
- package/dist/internal/reminders/reminder-filter.js +34 -0
- package/dist/internal/reminders/reminder-format.d.ts +6 -0
- package/dist/internal/reminders/reminder-format.js +20 -2
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/internal/stores/session-hover-store.d.ts +20 -5
- package/dist/internal/stores/session-hover-store.js +31 -13
- package/dist/internal/stores/session-panel-memory-store.js +2 -0
- package/dist/primitives/count-badge.d.ts +3 -1
- package/dist/primitives/count-badge.js +3 -3
- package/dist/primitives/glyph-paths-source.js +1 -1
- package/dist/primitives/glyph-paths.generated.js +1 -1
- package/dist/providers/ui-provider.d.ts +57 -0
- package/dist/providers/ui-provider.js +4 -0
- package/dist/session/composer/ComposerCard.d.ts +2 -0
- package/dist/session/composer/ComposerCard.js +5 -1
- package/dist/session/composer/agent-selector.js +12 -5
- package/dist/session/composer/composer-toolbar.js +4 -2
- package/dist/session/composer/composer-underbar.js +3 -1
- package/dist/session/composer/composer.js +34 -5
- package/dist/session/composer/menus/slash-actions.d.ts +22 -2
- package/dist/session/composer/menus/slash-actions.js +20 -0
- package/dist/session/composer/menus/slash-items.js +19 -3
- package/dist/session/header/reminder-parts.d.ts +285 -0
- package/dist/session/header/reminder-parts.js +115 -0
- package/dist/session/header/session-reminders-indicator.d.ts +13 -0
- package/dist/session/header/session-reminders-indicator.js +142 -35
- package/dist/session/models/model-availability.d.ts +23 -0
- package/dist/session/models/model-availability.js +20 -0
- package/dist/session/models/model-grouping.js +1 -4
- package/dist/session/models/model-selector.js +23 -14
- package/dist/session/models/use-model-connection-gate.js +4 -1
- package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
- package/dist/session/outcomes/session-outcomes-provider.js +31 -3
- package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
- package/dist/session/overrides/session-overrides-composer.js +5 -1
- package/dist/session/panel/mobile-tool-drawer.js +4 -2
- package/dist/session/panel/open-session-quick-view.js +2 -1
- package/dist/session/panel/panel-fallback.js +8 -4
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +13 -1
- package/dist/session/panel/session-reminders-panel.d.ts +4 -0
- package/dist/session/panel/session-reminders-panel.js +109 -0
- package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
- package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
- package/dist/session/print/session-print-header.d.ts +0 -10
- package/dist/session/print/session-print-header.js +16 -1
- package/dist/session/status/session-surface.js +1 -1
- package/dist/session/thread/optimistic-turn.d.ts +5 -1
- package/dist/session/thread/optimistic-turn.js +8 -6
- package/dist/session/thread/session-chat.d.ts +6 -1
- package/dist/session/thread/session-chat.js +22 -3
- package/dist/session/thread/session-thread.d.ts +10 -1
- package/dist/session/thread/session-thread.js +2 -2
- package/dist/session/turn/burst-step-body.js +4 -0
- package/dist/styles.css +1 -1
- package/dist/workspace/sidebar/project-session-list.js +52 -55
- package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
- package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
- package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
- package/dist/workspace/sidebar/session-status-mark.js +63 -17
- package/package.json +7 -3
- package/skills/kortix-ui/SKILL.md +1 -1
- package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
- package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { qk, useFeatureFlag } from '@kortix/sdk/react';
|
|
3
|
-
import {
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { useCallback, useState, useSyncExternalStore } from 'react';
|
|
5
5
|
export const NOTIFICATION_CENTER_FLAG = 'notification_center';
|
|
6
6
|
/** The predicate of `useFeatureFlag`: the server said exactly `true`. */
|
|
@@ -50,14 +50,14 @@ export function anyCachedNotificationCenter(client) {
|
|
|
50
50
|
return client.getQueryCache().getAll().some(holdsFlagOn);
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* `
|
|
54
|
-
* `
|
|
53
|
+
* A `useSyncExternalStore` subscription to the query cache, live only while
|
|
54
|
+
* `active`. The cache reports synchronously, also when another component's
|
|
55
55
|
* `useQuery` builds its query during render; the re-check waits for a
|
|
56
56
|
* microtask so this component never updates in the middle of that render.
|
|
57
57
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
if (!
|
|
58
|
+
function useCacheSubscription(client, active) {
|
|
59
|
+
return useCallback((onChange) => {
|
|
60
|
+
if (!active)
|
|
61
61
|
return () => { };
|
|
62
62
|
let live = true;
|
|
63
63
|
const unsubscribe = client.getQueryCache().subscribe(() => {
|
|
@@ -70,8 +70,11 @@ function useAnyCachedNotificationCenter(client, scan) {
|
|
|
70
70
|
live = false;
|
|
71
71
|
unsubscribe();
|
|
72
72
|
};
|
|
73
|
-
}, [client,
|
|
74
|
-
|
|
73
|
+
}, [client, active]);
|
|
74
|
+
}
|
|
75
|
+
/** `anyCachedNotificationCenter`, reactive. Subscribes to the cache only while `scan`. */
|
|
76
|
+
function useAnyCachedNotificationCenter(client, scan) {
|
|
77
|
+
return useSyncExternalStore(useCacheSubscription(client, scan), () => scan && anyCachedNotificationCenter(client), () => false);
|
|
75
78
|
}
|
|
76
79
|
/** Is the notification center on for `projectId`, or, without one, for any cached project. */
|
|
77
80
|
export function useNotificationCenter(projectId) {
|
|
@@ -88,14 +91,17 @@ export function useNotificationCenter(projectId) {
|
|
|
88
91
|
* load of a project page starts off.
|
|
89
92
|
*/
|
|
90
93
|
export function useNotificationHostGate(projectId) {
|
|
91
|
-
// The global host sits outside the access boundary. Observe the shell's
|
|
92
|
-
// detail entry without starting a request for a potentially revoked project.
|
|
93
|
-
const detail = useQuery({
|
|
94
|
-
queryKey: qk.project.detail(projectId ?? ''),
|
|
95
|
-
enabled: false,
|
|
96
|
-
});
|
|
97
|
-
const flag = { isLoading: detail.isPending, enabled: notificationCenterOn(detail.data?.project) };
|
|
98
94
|
const client = useQueryClient();
|
|
95
|
+
// The global host sits outside the access boundary. Read the shell's detail
|
|
96
|
+
// entry from the cache: no request for a potentially revoked project, and no
|
|
97
|
+
// observer. An observer added a detail query without a queryFn (also for the
|
|
98
|
+
// empty id of a page outside any project), and every refetch of it logged
|
|
99
|
+
// "No queryFn was passed".
|
|
100
|
+
const detail = useSyncExternalStore(useCacheSubscription(client, !!projectId), () => (projectId ? client.getQueryState(qk.project.detail(projectId)) : undefined), () => undefined);
|
|
101
|
+
const flag = {
|
|
102
|
+
isLoading: !detail || detail.status === 'pending',
|
|
103
|
+
enabled: notificationCenterOn(detail?.data?.project),
|
|
104
|
+
};
|
|
99
105
|
const anyProject = useAnyCachedNotificationCenter(client, !projectId);
|
|
100
106
|
const [previous, setPrevious] = useState(false);
|
|
101
107
|
let answer = anyProject;
|
|
@@ -51,3 +51,9 @@ export declare function useIsNeedsInputPoller(projectId: string): boolean;
|
|
|
51
51
|
* `ProjectSessionList` alone still sees open questions.
|
|
52
52
|
*/
|
|
53
53
|
export declare function useSessionNeedsYouCounts(projectId: string): Record<string, number>;
|
|
54
|
+
/**
|
|
55
|
+
* Sessions with an open agent question or a pending connector approval: the
|
|
56
|
+
* `needs-you` rows that wait on an answer rather than a change request. Reads
|
|
57
|
+
* the cache `useSessionNeedsYouCounts` already polls, so it adds no request.
|
|
58
|
+
*/
|
|
59
|
+
export declare function useSessionsAwaitingAnswer(projectId: string): ReadonlySet<string>;
|
|
@@ -82,3 +82,14 @@ export function useSessionNeedsYouCounts(projectId) {
|
|
|
82
82
|
const { data: needsInput } = useSessionsNeedingInput(projectId, { enabled: poller });
|
|
83
83
|
return useMemo(() => withNeedsInput(needsYouBySession, needsInput), [needsYouBySession, needsInput]);
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Sessions with an open agent question or a pending connector approval: the
|
|
87
|
+
* `needs-you` rows that wait on an answer rather than a change request. Reads
|
|
88
|
+
* the cache `useSessionNeedsYouCounts` already polls, so it adds no request.
|
|
89
|
+
*/
|
|
90
|
+
export function useSessionsAwaitingAnswer(projectId) {
|
|
91
|
+
const { data } = useSessionsNeedingInput(projectId, { enabled: false });
|
|
92
|
+
return useMemo(() => new Set(Object.entries(data?.sessions ?? {})
|
|
93
|
+
.filter(([, count]) => count > 0)
|
|
94
|
+
.map(([sessionId]) => sessionId)), [data]);
|
|
95
|
+
}
|
|
@@ -10,7 +10,7 @@ export { isConnectionClosedNoise, isFailedToSendMessageNoise, isFramelessNetwork
|
|
|
10
10
|
export { isIosWebViewInjectedStackOverflowNoise, isOldBrowserDomNullDerefNoise, isFirefoxDeadObjectUnattributableNoise, isOldBrowserSyntaxParseError, isOldWebkitRegexNoiseMessage, isUndefinedVariableThirdPartyNoise, isUnresolvableStackOverflowNoise, } from './browser-noise/rules/old-browsers.js';
|
|
11
11
|
export { isEmbedPdfTilingReactUpdateDepthNoise, isEmbedPdfTilingTileDestructureNoise, } from './browser-noise/rules/pdf.js';
|
|
12
12
|
export { isDocumentStateNotFoundNoise, isExpectedNextRecoveryBailoutNoise, isFirefoxReactSchedulerReentryNoise, isLikelyDomMutationNoise, isServerSuspenseBailoutNoise, isThirdPartyReactUpdateDepthNoise, } from './browser-noise/rules/react.js';
|
|
13
|
-
export { isAnonymousAuthRefreshRace, isAutoplayNotAllowedRejectionNoise, isNonErrorObjectNotFoundRejectionNoise, isNonErrorUndefinedRejectionNoise, isOperationErrorPopErrorScopeNoise, isSupabaseTokenExpiredNoise, } from './browser-noise/rules/rejections.js';
|
|
13
|
+
export { isAnonymousAuthRefreshRace, isAutoplayNotAllowedRejectionNoise, isNonErrorObjectNotFoundRejectionNoise, isNonErrorUndefinedRejectionNoise, isOperationErrorPopErrorScopeNoise, isPlayNotSupportedRejectionNoise, isSupabaseTokenExpiredNoise, } from './browser-noise/rules/rejections.js';
|
|
14
14
|
export { isSafariGenericSecurityErrorNoise, isStorageDisabledWebViewNoiseMessage, isStorageSecurityErrorNoise, } from './browser-noise/rules/storage.js';
|
|
15
15
|
export { isAndroidWebViewNativeBridgePostEventNoise, isAndroidWebViewNativeBridgePostMessageNoise, isIOSWebViewWebKitBridgeNoise, } from './browser-noise/rules/webview.js';
|
|
16
16
|
export declare const NOISE_RULES: readonly NoiseRule[];
|
|
@@ -35,7 +35,7 @@ export { isConnectionClosedNoise, isFailedToSendMessageNoise, isFramelessNetwork
|
|
|
35
35
|
export { isIosWebViewInjectedStackOverflowNoise, isOldBrowserDomNullDerefNoise, isFirefoxDeadObjectUnattributableNoise, isOldBrowserSyntaxParseError, isOldWebkitRegexNoiseMessage, isUndefinedVariableThirdPartyNoise, isUnresolvableStackOverflowNoise, } from './browser-noise/rules/old-browsers.js';
|
|
36
36
|
export { isEmbedPdfTilingReactUpdateDepthNoise, isEmbedPdfTilingTileDestructureNoise, } from './browser-noise/rules/pdf.js';
|
|
37
37
|
export { isDocumentStateNotFoundNoise, isExpectedNextRecoveryBailoutNoise, isFirefoxReactSchedulerReentryNoise, isLikelyDomMutationNoise, isServerSuspenseBailoutNoise, isThirdPartyReactUpdateDepthNoise, } from './browser-noise/rules/react.js';
|
|
38
|
-
export { isAnonymousAuthRefreshRace, isAutoplayNotAllowedRejectionNoise, isNonErrorObjectNotFoundRejectionNoise, isNonErrorUndefinedRejectionNoise, isOperationErrorPopErrorScopeNoise, isSupabaseTokenExpiredNoise, } from './browser-noise/rules/rejections.js';
|
|
38
|
+
export { isAnonymousAuthRefreshRace, isAutoplayNotAllowedRejectionNoise, isNonErrorObjectNotFoundRejectionNoise, isNonErrorUndefinedRejectionNoise, isOperationErrorPopErrorScopeNoise, isPlayNotSupportedRejectionNoise, isSupabaseTokenExpiredNoise, } from './browser-noise/rules/rejections.js';
|
|
39
39
|
export { isSafariGenericSecurityErrorNoise, isStorageDisabledWebViewNoiseMessage, isStorageSecurityErrorNoise, } from './browser-noise/rules/storage.js';
|
|
40
40
|
export { isAndroidWebViewNativeBridgePostEventNoise, isAndroidWebViewNativeBridgePostMessageNoise, isIOSWebViewWebKitBridgeNoise, } from './browser-noise/rules/webview.js';
|
|
41
41
|
export const NOISE_RULES = [
|
|
@@ -127,4 +127,14 @@ export declare function isAutoplayNotAllowedRejectionNoise(input: {
|
|
|
127
127
|
filename?: unknown;
|
|
128
128
|
} | undefined>;
|
|
129
129
|
}): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Whether a Sentry event is the frameless `HTMLMediaElement.play()`
|
|
132
|
+
* `NotSupportedError` rejection. See `PLAY_NOT_SUPPORTED_PATTERN`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function isPlayNotSupportedRejectionNoise(input: {
|
|
135
|
+
message?: unknown;
|
|
136
|
+
frames?: Array<{
|
|
137
|
+
filename?: unknown;
|
|
138
|
+
} | undefined>;
|
|
139
|
+
}): boolean;
|
|
130
140
|
export declare const REJECTION_RULES: readonly NoiseRule[];
|
|
@@ -479,7 +479,28 @@ export function isAutoplayNotAllowedRejectionNoise(input) {
|
|
|
479
479
|
if (!PLAY_NOT_ALLOWED_PATTERN.test(message)) {
|
|
480
480
|
return false;
|
|
481
481
|
}
|
|
482
|
-
|
|
482
|
+
return isUnattributablePlayRejection(input.frames);
|
|
483
|
+
}
|
|
484
|
+
// Firefox's `NotSupportedError` for a play() promise whose element has no
|
|
485
|
+
// playable source. Better Stack pattern
|
|
486
|
+
// 1b3195a99afc344886a33243b1b73942b94e74c6bf5ca6b561d3bddc3e3289c6
|
|
487
|
+
// (Kortix Frontend prod, application_id 2346967, KRTX-2998): 1 occurrence, 0
|
|
488
|
+
// identified users, first=last 2026-10-21T09:54:06Z, call site unknown. Every
|
|
489
|
+
// first-party play() promise attaches a `.catch` (`film.tsx`, `sounds.ts`,
|
|
490
|
+
// `video-renderer.tsx`), so the frameless capture is not ours. The guards of
|
|
491
|
+
// `isAutoplayNotAllowedRejectionNoise` keep an attributable one reporting.
|
|
492
|
+
const PLAY_NOT_SUPPORTED_PATTERN = /^The media resource indicated by the src attribute or assigned media provider object was not suitable\.$/;
|
|
493
|
+
/**
|
|
494
|
+
* Whether a Sentry event is the frameless `HTMLMediaElement.play()`
|
|
495
|
+
* `NotSupportedError` rejection. See `PLAY_NOT_SUPPORTED_PATTERN`.
|
|
496
|
+
*/
|
|
497
|
+
export function isPlayNotSupportedRejectionNoise(input) {
|
|
498
|
+
if (!PLAY_NOT_SUPPORTED_PATTERN.test(normalizeString(input.message))) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return isUnattributablePlayRejection(input.frames);
|
|
502
|
+
}
|
|
503
|
+
function isUnattributablePlayRejection(frames = []) {
|
|
483
504
|
// Negative guard #1: a resolved first-party `apps/web/src/…` frame means
|
|
484
505
|
// our own code left a play promise unhandled → actionable; keep reporting
|
|
485
506
|
// so the call site can be found + given its `.catch`.
|
|
@@ -501,6 +522,11 @@ export const REJECTION_RULES = [
|
|
|
501
522
|
appliesTo: 'sentry',
|
|
502
523
|
match: isAutoplayNotAllowedRejectionNoise,
|
|
503
524
|
},
|
|
525
|
+
{
|
|
526
|
+
id: 'play-not-supported',
|
|
527
|
+
appliesTo: 'sentry',
|
|
528
|
+
match: isPlayNotSupportedRejectionNoise,
|
|
529
|
+
},
|
|
504
530
|
{
|
|
505
531
|
id: 'non-error-undefined-rejection',
|
|
506
532
|
appliesTo: 'sentry',
|
|
@@ -100,6 +100,10 @@ const CATALOG_PROVIDER_IDS = new Set(CATALOG_PROVIDER_ENV.map((provider) => prov
|
|
|
100
100
|
* (a custom provider) keeps initials.
|
|
101
101
|
*/
|
|
102
102
|
export function providerIconSrc(providerID, baseUrl) {
|
|
103
|
+
// Drawn in code (ProviderLogo), never fetched: models.dev has no Platinum logo
|
|
104
|
+
// and answers with its generic sparkles mark.
|
|
105
|
+
if (providerID === 'platinum')
|
|
106
|
+
return undefined;
|
|
103
107
|
const icon = PROVIDER_ICON_MAP[providerID]?.icon;
|
|
104
108
|
if (icon)
|
|
105
109
|
return baseUrl === undefined ? `https://models.dev/logos/${icon}.svg` : `${baseUrl}/${icon}.svg`;
|
|
@@ -144,7 +148,8 @@ export function ProviderLogo({ providerID, name, className, size = 'default', to
|
|
|
144
148
|
// An unreachable models.dev logo (offline self-host) falls back to initials.
|
|
145
149
|
const [failedSrc, setFailedSrc] = useState();
|
|
146
150
|
const kortix = providerID === 'kortix';
|
|
147
|
-
const
|
|
151
|
+
const platinum = providerID === 'platinum';
|
|
152
|
+
const iconSrc = kortix || platinum ? undefined : providerIconSrc(providerID, providerIconBaseUrl);
|
|
148
153
|
const src = iconSrc === failedSrc ? undefined : iconSrc;
|
|
149
154
|
const sizeClasses = {
|
|
150
155
|
xs: 'size-4',
|
|
@@ -166,10 +171,16 @@ export function ProviderLogo({ providerID, name, className, size = 'default', to
|
|
|
166
171
|
// the other sizes read as an avatar, and at 16px a tile around a 12px
|
|
167
172
|
// mark is all frame and no logo. The initials fallback keeps its tile
|
|
168
173
|
// at every size: two bare letters at 9px read as debris, not a logo.
|
|
169
|
-
(src || kortix) && size === 'xs' ? 'bg-transparent' : light ? LOGO_TILE_LIGHT : 'bg-muted rounded-md', sizeClasses[size], className), "aria-hidden": "true", children: kortix ? (
|
|
174
|
+
(src || kortix || platinum) && size === 'xs' ? 'bg-transparent' : light ? LOGO_TILE_LIGHT : 'bg-muted rounded-md', sizeClasses[size], className), "aria-hidden": "true", children: kortix ? (
|
|
170
175
|
// The host's brand mark (`useBrand` icon), else the Kortix symbol in currentColor.
|
|
171
176
|
_jsx(KortixLogo, { variant: "icon", style: {
|
|
172
177
|
width: `calc(var(--spacing) * ${KORTIX_MARK_STEPS[size]})`,
|
|
173
178
|
height: `calc(var(--spacing) * ${KORTIX_MARK_STEPS[size]})`,
|
|
174
|
-
}, className: light ? LOGO_TILE_LIGHT_TEXT : 'text-foreground' })) :
|
|
179
|
+
}, className: light ? LOGO_TILE_LIGHT_TEXT : 'text-foreground' })) : platinum ? (
|
|
180
|
+
// The Platinum mark (platinum.dev/brand): a flat square in currentColor,
|
|
181
|
+
// the favicon geometry — 14 of 24, inset 5.
|
|
182
|
+
_jsx("svg", { viewBox: "0 0 24 24", style: {
|
|
183
|
+
width: `calc(var(--spacing) * ${KORTIX_MARK_STEPS[size] + 2})`,
|
|
184
|
+
height: `calc(var(--spacing) * ${KORTIX_MARK_STEPS[size] + 2})`,
|
|
185
|
+
}, className: light ? LOGO_TILE_LIGHT_TEXT : 'text-foreground', children: _jsx("rect", { x: "5", y: "5", width: "14", height: "14", fill: "currentColor" }) })) : src ? (_jsx(Image, { src: src, alt: "", width: iconSizes[size], height: iconSizes[size], unoptimized: src.startsWith('https:'), onError: () => setFailedSrc(src), className: cn('object-contain', !light && 'dark:invert') })) : (_jsx("span", { className: cn('font-semibold tracking-wide uppercase', light ? LOGO_TILE_LIGHT_TEXT : 'text-muted-foreground', size === 'xs' ? 'text-[9px]' : 'text-xs'), children: initialsFor(providerID, name) })) }));
|
|
175
186
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
import type { UiTranslator } from '../../i18n/index.js';
|
|
3
|
+
import type { ProjectTrigger } from '@kortix/sdk';
|
|
4
|
+
/**
|
|
5
|
+
* The trigger clock's health tones and its "last run" label. The Triggers list
|
|
6
|
+
* (`customize/triggers/schedule`) and the session reminders panel
|
|
7
|
+
* (`session/panel`) both draw them, so they sit below both layers.
|
|
8
|
+
*/
|
|
9
|
+
export type ClockState = 'ok' | 'failed' | 'needs_connection' | 'pending' | 'paused' | 'never';
|
|
10
|
+
/**
|
|
11
|
+
* ONE tone per state drives both the reflection and the dot, so they cannot
|
|
12
|
+
* disagree. Three colours, always full strength: green = live (running, ran,
|
|
13
|
+
* or not run yet), red = failed, yellow = paused or waiting on setup.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CLOCK_TONE: {
|
|
16
|
+
readonly ok: {
|
|
17
|
+
readonly reflection: "text-kortix-green";
|
|
18
|
+
readonly dot: "bg-kortix-green";
|
|
19
|
+
};
|
|
20
|
+
readonly never: {
|
|
21
|
+
readonly reflection: "text-kortix-green";
|
|
22
|
+
readonly dot: "bg-kortix-green";
|
|
23
|
+
};
|
|
24
|
+
readonly failed: {
|
|
25
|
+
readonly reflection: "text-kortix-red";
|
|
26
|
+
readonly dot: "bg-kortix-red";
|
|
27
|
+
};
|
|
28
|
+
readonly needs_connection: {
|
|
29
|
+
readonly reflection: "text-kortix-yellow";
|
|
30
|
+
readonly dot: "bg-kortix-yellow";
|
|
31
|
+
};
|
|
32
|
+
readonly pending: {
|
|
33
|
+
readonly reflection: "text-kortix-yellow";
|
|
34
|
+
readonly dot: "bg-kortix-yellow";
|
|
35
|
+
};
|
|
36
|
+
readonly paused: {
|
|
37
|
+
readonly reflection: "text-kortix-yellow";
|
|
38
|
+
readonly dot: "bg-kortix-yellow";
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/** "2d ago", "9h ago", "3w ago" in the viewer's locale, or "Never run". */
|
|
42
|
+
export declare function lastRunLabel(trigger: Pick<ProjectTrigger, 'last_fired_at' | 'event'>, t: UiTranslator, locale: string, now?: number): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const GREEN = { reflection: 'text-kortix-green', dot: 'bg-kortix-green' };
|
|
2
|
+
const RED = { reflection: 'text-kortix-red', dot: 'bg-kortix-red' };
|
|
3
|
+
const YELLOW = { reflection: 'text-kortix-yellow', dot: 'bg-kortix-yellow' };
|
|
4
|
+
/**
|
|
5
|
+
* ONE tone per state drives both the reflection and the dot, so they cannot
|
|
6
|
+
* disagree. Three colours, always full strength: green = live (running, ran,
|
|
7
|
+
* or not run yet), red = failed, yellow = paused or waiting on setup.
|
|
8
|
+
*/
|
|
9
|
+
export const CLOCK_TONE = {
|
|
10
|
+
ok: GREEN,
|
|
11
|
+
never: GREEN,
|
|
12
|
+
failed: RED,
|
|
13
|
+
needs_connection: YELLOW,
|
|
14
|
+
pending: YELLOW,
|
|
15
|
+
paused: YELLOW,
|
|
16
|
+
};
|
|
17
|
+
/** "2d ago", "9h ago", "3w ago" in the viewer's locale, or "Never run". */
|
|
18
|
+
export function lastRunLabel(trigger, t, locale, now = Date.now()) {
|
|
19
|
+
const iso = trigger.last_fired_at ?? trigger.event?.last_event_at ?? null;
|
|
20
|
+
const at = iso ? Date.parse(iso) : Number.NaN;
|
|
21
|
+
if (!Number.isFinite(at))
|
|
22
|
+
return t.raw('text3d40a69d3160');
|
|
23
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { style: 'narrow', numeric: 'auto' });
|
|
24
|
+
const min = Math.floor(Math.max(0, now - at) / 60_000);
|
|
25
|
+
if (min < 1)
|
|
26
|
+
return rtf.format(0, 'second');
|
|
27
|
+
if (min < 60)
|
|
28
|
+
return rtf.format(-min, 'minute');
|
|
29
|
+
if (min < 24 * 60)
|
|
30
|
+
return rtf.format(-Math.floor(min / 60), 'hour');
|
|
31
|
+
if (min < 7 * 24 * 60)
|
|
32
|
+
return rtf.format(-Math.floor(min / (24 * 60)), 'day');
|
|
33
|
+
return rtf.format(-Math.floor(min / (7 * 24 * 60)), 'week');
|
|
34
|
+
}
|
|
@@ -36,6 +36,12 @@ export interface WebNotificationPayload {
|
|
|
36
36
|
preview?: string;
|
|
37
37
|
/** Localized "Open session" for the finished-turn toast. */
|
|
38
38
|
openSessionLabel?: string;
|
|
39
|
+
/** The open change request the finished turn produced: the toast offers Review. */
|
|
40
|
+
changeRequest?: {
|
|
41
|
+
crId: string;
|
|
42
|
+
detail: string;
|
|
43
|
+
reviewLabel: string;
|
|
44
|
+
};
|
|
39
45
|
/** Localized "{count} tasks finished" for the stack several turns collapse into. */
|
|
40
46
|
stackTitle?: (count: number) => string;
|
|
41
47
|
/** A row of the notification inbox. Inbox rows exist only for projects with `notification_center` on. */
|
|
@@ -31,7 +31,7 @@ import { createElement } from 'react';
|
|
|
31
31
|
import { cachedNotificationCenter, cachedNotificationCenterAnswer, } from '../hooks/use-notification-center.js';
|
|
32
32
|
import { hasWebPushSubscription } from '../notifications/web-push.js';
|
|
33
33
|
import { customToast, dismissToast, ToastCard, toastIcon, errorToast, infoToast, successToast, warningToast, } from '../../primitives/toast.js';
|
|
34
|
-
import { ClockIcon } from '@phosphor-icons/react';
|
|
34
|
+
import { ClockIcon, GitPullRequestIcon } from '@phosphor-icons/react';
|
|
35
35
|
import { SessionFailedCard, SessionStackCard, } from '../notifications/session-toast-cards.js';
|
|
36
36
|
import { logger } from './logger.js';
|
|
37
37
|
import { softNavigate } from './navigation/router-bridge.js';
|
|
@@ -42,7 +42,7 @@ import { playSound } from './sounds.js';
|
|
|
42
42
|
import { openTabAndNavigate, useTabStore } from '../stores/tab-store.js';
|
|
43
43
|
import { useTurnAttentionStore } from '../stores/turn-attention-store.js';
|
|
44
44
|
import { useWebNotificationStore } from '../stores/web-notification-store.js';
|
|
45
|
-
import { normalizeAppPathname } from '@kortix/sdk';
|
|
45
|
+
import { listChangeRequests, normalizeAppPathname, } from '@kortix/sdk';
|
|
46
46
|
// ============================================================================
|
|
47
47
|
// Server push preference
|
|
48
48
|
// ============================================================================
|
|
@@ -138,7 +138,7 @@ function navigateToSession(sessionId, sessionTitle, opts) {
|
|
|
138
138
|
const projectId = opts?.projectId ?? currentProjectId();
|
|
139
139
|
if (!projectId)
|
|
140
140
|
return;
|
|
141
|
-
const href = projectSessionHref(projectId, sessionId);
|
|
141
|
+
const href = projectSessionHref(projectId, sessionId) + (opts?.search ?? '');
|
|
142
142
|
try {
|
|
143
143
|
// Open/activate the tab in the tab store + pushState
|
|
144
144
|
openTabAndNavigate({
|
|
@@ -497,6 +497,15 @@ function showSessionToast(payload, sessionId) {
|
|
|
497
497
|
// focus) names nothing and opens nothing: the person is already there.
|
|
498
498
|
// Read at render, so it follows where the person is now.
|
|
499
499
|
const elsewhere = () => !isViewingSession(sessionId);
|
|
500
|
+
const cr = payload.changeRequest;
|
|
501
|
+
// `?cr=` opens the change request's review dialog on the session page.
|
|
502
|
+
const review = () => {
|
|
503
|
+
navigateToSession(sessionId, payload.body, {
|
|
504
|
+
projectId: payload.projectId,
|
|
505
|
+
search: cr ? `?cr=${encodeURIComponent(cr.crId)}` : undefined,
|
|
506
|
+
});
|
|
507
|
+
payload.onClick?.();
|
|
508
|
+
};
|
|
500
509
|
if (item.failed) {
|
|
501
510
|
customToast((dismiss) => createElement(SessionFailedCard, {
|
|
502
511
|
title: payload.inAppTitle ?? payload.title,
|
|
@@ -512,12 +521,23 @@ function showSessionToast(payload, sessionId) {
|
|
|
512
521
|
message: payload.inAppTitle ?? payload.title,
|
|
513
522
|
icon: toastIcon('success'),
|
|
514
523
|
variant: 'success',
|
|
524
|
+
// On the session itself the change request card is in the transcript,
|
|
525
|
+
// so the toast only says it is there.
|
|
515
526
|
options: elsewhere()
|
|
516
527
|
? {
|
|
517
|
-
entity: {
|
|
518
|
-
|
|
528
|
+
entity: {
|
|
529
|
+
icon: cr ? createElement(GitPullRequestIcon, { className: 'size-3.5' }) : undefined,
|
|
530
|
+
label: item.label,
|
|
531
|
+
},
|
|
532
|
+
description: cr?.detail,
|
|
533
|
+
actions: [
|
|
534
|
+
...(cr ? [{ label: cr.reviewLabel, onClick: review }] : []),
|
|
535
|
+
{ label: payload.openSessionLabel ?? openLabel, onClick: () => open(item) },
|
|
536
|
+
],
|
|
519
537
|
}
|
|
520
|
-
:
|
|
538
|
+
: cr
|
|
539
|
+
? { description: cr.detail }
|
|
540
|
+
: undefined,
|
|
521
541
|
onDismiss: dismiss,
|
|
522
542
|
}), { id, duration: NOTIFICATION_VISIBLE_MS, onClose });
|
|
523
543
|
}
|
|
@@ -576,7 +596,7 @@ export function notifyTaskCompleteFor({ sessionId, sessionTitle, projectId, noti
|
|
|
576
596
|
noteTurnCompleteShown(sessionId);
|
|
577
597
|
const name = sessionDisplayName(sessionTitle, sessionId);
|
|
578
598
|
const label = sessionTitle ? `"${name.slice(0, 60)}"` : name;
|
|
579
|
-
|
|
599
|
+
const payload = {
|
|
580
600
|
type: 'completion',
|
|
581
601
|
entityLabel: name,
|
|
582
602
|
openSessionLabel: tI18nComplete.raw('textb205bb47f81a'),
|
|
@@ -591,8 +611,38 @@ export function notifyTaskCompleteFor({ sessionId, sessionTitle, projectId, noti
|
|
|
591
611
|
// deep link (the same rule the click-time path documents above).
|
|
592
612
|
projectId,
|
|
593
613
|
notificationCenter,
|
|
614
|
+
};
|
|
615
|
+
if (!projectId) {
|
|
616
|
+
sendWebNotification(payload);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
void openChangeRequestOf(projectId, sessionId).then((cr) => {
|
|
620
|
+
if (!cr) {
|
|
621
|
+
sendWebNotification(payload);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const detail = tI18nComplete('text57f57edb6f16', { number: cr.number });
|
|
625
|
+
sendWebNotification({
|
|
626
|
+
...payload,
|
|
627
|
+
body: `${payload.body} ${detail}`,
|
|
628
|
+
changeRequest: { crId: cr.cr_id, detail, reviewLabel: tI18nComplete.raw('textaff0766a5290') },
|
|
629
|
+
});
|
|
594
630
|
});
|
|
595
631
|
}
|
|
632
|
+
/** How long the finished-turn toast waits for the change request lookup. */
|
|
633
|
+
const CHANGE_REQUEST_LOOKUP_MS = 2_500;
|
|
634
|
+
/**
|
|
635
|
+
* The newest open change request the session opened, or null. Server-vouched
|
|
636
|
+
* by `origin_session_id`, the same rule as the transcript's change request card.
|
|
637
|
+
* A slow or failed lookup answers null, so the toast never waits on it for long.
|
|
638
|
+
*/
|
|
639
|
+
function openChangeRequestOf(projectId, sessionId) {
|
|
640
|
+
const lookup = listChangeRequests(projectId, 'open', { originSessionId: sessionId, limit: 1 })
|
|
641
|
+
.then((page) => page.change_requests[0] ?? null)
|
|
642
|
+
.catch(() => null);
|
|
643
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve(null), CHANGE_REQUEST_LOOKUP_MS));
|
|
644
|
+
return Promise.race([lookup, timeout]);
|
|
645
|
+
}
|
|
596
646
|
/**
|
|
597
647
|
* Notify that a session encountered an error.
|
|
598
648
|
*/
|
|
@@ -14,6 +14,19 @@ export interface NotificationPanelProps {
|
|
|
14
14
|
}
|
|
15
15
|
/** Props only: the header, the All / Needs you / Done tabs, the rows, and the footer offer. */
|
|
16
16
|
export declare function NotificationPanel({ rows, state, unreadCount, prompt, onOpenRow, onMarkAllRead, onRetry, onTurnOn, onDismissPrompt, }: NotificationPanelProps): import("react").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* The bell (Paper B12 in the toast's badge-glow style): the glyph muted, then
|
|
19
|
+
* again in the badge's colour under `BELL_REFLECTION_MASK`, so the
|
|
20
|
+
* colour reaches in from the corner the count sits on, as `EntityTile` in
|
|
21
|
+
* `components/ui/toast.tsx` and `TriggerTile` draw it. Yellow when something
|
|
22
|
+
* waits on the person (the Needs you tab), blue otherwise.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The toast's `REFLECTION_MASK` scaled from its 20px icon to the 16px bell
|
|
26
|
+
* (16px → 12px), centred on the glyph's bottom-right corner where the count
|
|
27
|
+
* sits. At full size it would tint the whole bell.
|
|
28
|
+
*/
|
|
29
|
+
export declare const BELL_REFLECTION_MASK = "radial-gradient(circle 12px at 100% 100%, #000 35%, transparent 100%)";
|
|
17
30
|
export declare function BellMark({ count, needsYou, surface, }: {
|
|
18
31
|
count: number;
|
|
19
32
|
needsYou: boolean;
|
|
@@ -78,7 +78,7 @@ export function NotificationPanel({ rows, state, unreadCount, prompt, onOpenRow,
|
|
|
78
78
|
* (16px → 12px), centred on the glyph's bottom-right corner where the count
|
|
79
79
|
* sits. At full size it would tint the whole bell.
|
|
80
80
|
*/
|
|
81
|
-
const BELL_REFLECTION_MASK = 'radial-gradient(circle 12px at 100% 100%, #000 35%, transparent 100%)';
|
|
81
|
+
export const BELL_REFLECTION_MASK = 'radial-gradient(circle 12px at 100% 100%, #000 35%, transparent 100%)';
|
|
82
82
|
export function BellMark({ count, needsYou, surface = 'bg-background', }) {
|
|
83
83
|
const tone = needsYou
|
|
84
84
|
? { reflection: 'text-kortix-yellow', disc: 'bg-kortix-yellow' }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
import type { SessionReminder } from '@kortix/sdk';
|
|
3
|
+
export type ReminderStatusFilter = 'all' | 'active' | 'paused' | 'failed';
|
|
4
|
+
/** By next run: within the hour, the day, the week, or later than a week. */
|
|
5
|
+
export type ReminderTimeFilter = 'any' | 'hour' | 'day' | 'week' | 'later';
|
|
6
|
+
export declare const REMINDER_STATUS_FILTERS: readonly ReminderStatusFilter[];
|
|
7
|
+
export declare const REMINDER_TIME_FILTERS: readonly ReminderTimeFilter[];
|
|
8
|
+
export declare function matchesStatus(reminder: SessionReminder, status: ReminderStatusFilter): boolean;
|
|
9
|
+
/** A time filter other than `any` keeps only active reminders: a paused one has no next run. */
|
|
10
|
+
export declare function matchesTime(reminder: SessionReminder, time: ReminderTimeFilter, now: number): boolean;
|
|
11
|
+
/** Every word of the query appears in the title, the prompt or the schedule label. */
|
|
12
|
+
export declare function matchesQuery(reminder: SessionReminder, query: string, schedule: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const REMINDER_STATUS_FILTERS = ['all', 'active', 'paused', 'failed'];
|
|
2
|
+
export const REMINDER_TIME_FILTERS = ['any', 'hour', 'day', 'week', 'later'];
|
|
3
|
+
const HOUR = 60 * 60 * 1000;
|
|
4
|
+
const WINDOW_MS = {
|
|
5
|
+
hour: HOUR,
|
|
6
|
+
day: 24 * HOUR,
|
|
7
|
+
week: 7 * 24 * HOUR,
|
|
8
|
+
};
|
|
9
|
+
export function matchesStatus(reminder, status) {
|
|
10
|
+
if (status === 'all')
|
|
11
|
+
return true;
|
|
12
|
+
if (status === 'failed')
|
|
13
|
+
return reminder.last_status === 'failed';
|
|
14
|
+
return status === 'active' ? reminder.state === 'active' : reminder.state !== 'active';
|
|
15
|
+
}
|
|
16
|
+
/** A time filter other than `any` keeps only active reminders: a paused one has no next run. */
|
|
17
|
+
export function matchesTime(reminder, time, now) {
|
|
18
|
+
if (time === 'any')
|
|
19
|
+
return true;
|
|
20
|
+
const next = reminder.state === 'active' && reminder.next_fire_at ? Date.parse(reminder.next_fire_at) : NaN;
|
|
21
|
+
if (!Number.isFinite(next))
|
|
22
|
+
return false;
|
|
23
|
+
if (time === 'later')
|
|
24
|
+
return next - now > WINDOW_MS.week;
|
|
25
|
+
return next - now <= WINDOW_MS[time];
|
|
26
|
+
}
|
|
27
|
+
/** Every word of the query appears in the title, the prompt or the schedule label. */
|
|
28
|
+
export function matchesQuery(reminder, query, schedule) {
|
|
29
|
+
const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
30
|
+
if (words.length === 0)
|
|
31
|
+
return true;
|
|
32
|
+
const haystack = [reminder.name, reminder.prompt, schedule].filter(Boolean).join(' ').toLowerCase();
|
|
33
|
+
return words.every((word) => haystack.includes(word));
|
|
34
|
+
}
|
|
@@ -14,6 +14,12 @@ export declare function formatFireTime(iso: string, locale: string, now: number)
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function nearestFire(reminders: readonly Pick<SessionReminder, 'state' | 'next_fire_at' | 'last_fired_at' | 'at'>[]): number | null;
|
|
16
16
|
export declare function soonestFire(reminders: readonly Pick<SessionReminder, 'state' | 'next_fire_at'>[]): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* How much of the wait before the next fire has passed, 0..1: the interval of
|
|
19
|
+
* an `every` reminder, else the span since the last fire (or creation). Null
|
|
20
|
+
* when there is no next fire to count toward.
|
|
21
|
+
*/
|
|
22
|
+
export declare function elapsedShare(reminder: Pick<SessionReminder, 'next_fire_at' | 'every_seconds' | 'last_fired_at' | 'created_at'>, now: number): number | null;
|
|
17
23
|
/** The first line of the reminder text, for a one-line row title. */
|
|
18
24
|
export declare function reminderTitle(reminder: Pick<SessionReminder, 'name' | 'prompt'>): string;
|
|
19
25
|
/** "Cron 0 9 * * * Europe/Berlin", "Every 1h" or "Once"; `t` is the `reminders` translator. */
|
|
@@ -10,10 +10,12 @@ export function formatFireTime(iso, locale, now) {
|
|
|
10
10
|
const deltaMin = Math.round((at - now) / 60_000);
|
|
11
11
|
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' });
|
|
12
12
|
const abs = Math.abs(deltaMin);
|
|
13
|
+
// `short` abbreviates with a period ("in 32 min."); a row's trailing label reads cleaner without it.
|
|
14
|
+
const relative = (value, unit) => rtf.format(value, unit).replace(/\.(?=\s|$)/g, '');
|
|
13
15
|
if (abs < 60)
|
|
14
|
-
return
|
|
16
|
+
return relative(deltaMin, 'minute');
|
|
15
17
|
if (abs < 24 * 60)
|
|
16
|
-
return
|
|
18
|
+
return relative(Math.round(deltaMin / 60), 'hour');
|
|
17
19
|
const date = new Date(at);
|
|
18
20
|
if (deltaMin > 0 && deltaMin < 7 * 24 * 60) {
|
|
19
21
|
return date.toLocaleString(locale, { weekday: 'short', hour: '2-digit', minute: '2-digit' });
|
|
@@ -60,6 +62,22 @@ export function soonestFire(reminders) {
|
|
|
60
62
|
}
|
|
61
63
|
return soonest;
|
|
62
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* How much of the wait before the next fire has passed, 0..1: the interval of
|
|
67
|
+
* an `every` reminder, else the span since the last fire (or creation). Null
|
|
68
|
+
* when there is no next fire to count toward.
|
|
69
|
+
*/
|
|
70
|
+
export function elapsedShare(reminder, now) {
|
|
71
|
+
const next = reminder.next_fire_at ? Date.parse(reminder.next_fire_at) : NaN;
|
|
72
|
+
if (!Number.isFinite(next))
|
|
73
|
+
return null;
|
|
74
|
+
const start = reminder.every_seconds
|
|
75
|
+
? next - reminder.every_seconds * 1000
|
|
76
|
+
: Date.parse(reminder.last_fired_at ?? reminder.created_at ?? '');
|
|
77
|
+
if (!Number.isFinite(start) || next <= start)
|
|
78
|
+
return null;
|
|
79
|
+
return Math.min(1, Math.max(0, (now - start) / (next - start)));
|
|
80
|
+
}
|
|
63
81
|
/** The first line of the reminder text, for a one-line row title. */
|
|
64
82
|
export function reminderTitle(reminder) {
|
|
65
83
|
return reminder.name ?? reminder.prompt.split('\n')[0].trim();
|
|
@@ -12,7 +12,7 @@ export interface ReadyChipState {
|
|
|
12
12
|
primaryName?: string;
|
|
13
13
|
}
|
|
14
14
|
/** The panel surfaces reachable from outside the panel. */
|
|
15
|
-
export type QuickView = 'terminal' | 'audit' | 'browser' | 'files';
|
|
15
|
+
export type QuickView = 'terminal' | 'audit' | 'browser' | 'files' | 'reminders';
|
|
16
16
|
/**
|
|
17
17
|
* Extra aim for a quick-view request — "open the browser" vs "open the browser
|
|
18
18
|
* ON THIS URL".
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* the active row is what is left, and it is also the cheaper shape: exactly one
|
|
12
12
|
* card is mounted at any time, by construction rather than by timing luck.
|
|
13
13
|
*
|
|
14
|
+
* The store also holds the active row's element and the one card's content
|
|
15
|
+
* slot (`SessionBriefCardHost`): rows render their brief into that slot, and
|
|
16
|
+
* the host glides to the row instead of a new card appearing at it.
|
|
17
|
+
*
|
|
14
18
|
* The delays live here rather than on the primitive for a performance reason.
|
|
15
19
|
* If `openDelay` were a prop derived from `warm`, every row in the sidebar would
|
|
16
20
|
* re-render each time the group warmed or cooled. Nothing selects `warm`, so it
|
|
@@ -20,15 +24,19 @@
|
|
|
20
24
|
/** The first card costs a delay, so a pointer crossing the list on its way
|
|
21
25
|
* somewhere else does not flash open every row it passes over. */
|
|
22
26
|
export declare const HOVER_OPEN_DELAY_MS = 200;
|
|
27
|
+
/** While a card is open, a move to another row waits this long, and a newer
|
|
28
|
+
* row restarts the wait: a pointer sweeping down the list moves the card once,
|
|
29
|
+
* to where it stops, instead of re-rendering it for every row it crosses. */
|
|
30
|
+
export declare const HOVER_MOVE_DEBOUNCE_MS = 50;
|
|
23
31
|
/** Long enough for the pointer to cross the `sideOffset` gap into the card. */
|
|
24
32
|
export declare const HOVER_CLOSE_DELAY_MS = 100;
|
|
25
33
|
/** How long the group stays warm after its last card closes. */
|
|
26
34
|
export declare const HOVER_WARM_GRACE_MS = 300;
|
|
27
35
|
/**
|
|
28
|
-
* Once one card is open the group is warm and the next row
|
|
29
|
-
* card reads as moving to the row under the pointer instead of
|
|
30
|
-
* reopening. This is the rule the design system already states for
|
|
31
|
-
* delay before the first, none for the rest
|
|
36
|
+
* Once one card is open the group is warm and the next row costs only the move
|
|
37
|
+
* debounce, so the card reads as moving to the row under the pointer instead of
|
|
38
|
+
* closing and reopening. This is the rule the design system already states for
|
|
39
|
+
* tooltips: delay before the first, (almost) none for the rest.
|
|
32
40
|
*/
|
|
33
41
|
export declare function hoverOpenDelayMs(warm: boolean): number;
|
|
34
42
|
/**
|
|
@@ -38,9 +46,16 @@ export declare function hoverOpenDelayMs(warm: boolean): number;
|
|
|
38
46
|
export declare function clearIfActive(current: string | null, sessionId: string): string | null;
|
|
39
47
|
interface SessionHoverState {
|
|
40
48
|
activeSessionId: string | null;
|
|
49
|
+
/** The active row's element: the card lines up with it. */
|
|
50
|
+
anchor: HTMLElement | null;
|
|
51
|
+
/** Keyboard focus moves the card without a glide. */
|
|
52
|
+
via: 'pointer' | 'focus';
|
|
53
|
+
/** The shared card's content slot, from `SessionBriefCardHost`. */
|
|
54
|
+
slot: HTMLElement | null;
|
|
55
|
+
setSlot: (slot: HTMLElement | null) => void;
|
|
41
56
|
/** Deliberately unselected by any component — see the note above. */
|
|
42
57
|
warm: boolean;
|
|
43
|
-
openSession: (sessionId: string) => void;
|
|
58
|
+
openSession: (sessionId: string, anchor?: HTMLElement, via?: 'pointer' | 'focus') => void;
|
|
44
59
|
closeSession: (sessionId: string) => void;
|
|
45
60
|
/** Immediate, for a click that opens something else and for unmount. */
|
|
46
61
|
dismiss: () => void;
|