@kortix/ui 0.14.4 → 0.14.6

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 (228) hide show
  1. package/README.md +8 -4
  2. package/dist/access/access/access-dialog.js +8 -5
  3. package/dist/access/access/copy-row.js +2 -2
  4. package/dist/access/sharing-picker.js +2 -1
  5. package/dist/browser/app-in-browser.js +5 -3
  6. package/dist/browser/browser-panel.js +6 -6
  7. package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
  8. package/dist/browser/sandbox-browser-chrome.js +4 -3
  9. package/dist/customize/agents/agent-access.d.ts +20 -0
  10. package/dist/customize/agents/agent-access.js +17 -0
  11. package/dist/customize/agents/agent-grant-pages.js +5 -6
  12. package/dist/customize/agents/agent-page.js +2 -1
  13. package/dist/customize/agents/agents-page.js +18 -1
  14. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  15. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  16. package/dist/customize/connectors/connector-routes.js +18 -0
  17. package/dist/customize/connectors/connectors-page.js +23 -4
  18. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  19. package/dist/customize/providers/provider-accordion.js +2 -1
  20. package/dist/customize/providers/provider-connect.js +2 -1
  21. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  22. package/dist/customize/sections/connector-connection-modal.js +11 -9
  23. package/dist/customize/sections/connectors-view.js +11 -13
  24. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  25. package/dist/customize/sections/llm-provider/constants.js +0 -2
  26. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  27. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  28. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  29. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  30. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  31. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  32. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  33. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  34. package/dist/customize/sections/llm-provider/utils.js +7 -14
  35. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  36. package/dist/customize/sections/view/gateway/gateway-routing.js +18 -20
  37. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  38. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  39. package/dist/customize/settings/rail-copy-context.js +5 -3
  40. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  41. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  42. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  43. package/dist/customize/shared/capability-tabs.js +2 -1
  44. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  45. package/dist/customize/shared/entity/entity-modal.js +7 -4
  46. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  47. package/dist/customize/skills/remove-skill-button.js +42 -0
  48. package/dist/customize/skills/skills-page.js +4 -6
  49. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  50. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  51. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  52. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  53. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  54. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  55. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  56. package/dist/customize/triggers/schedule/when-event.js +1 -1
  57. package/dist/diff/diff-view.js +9 -10
  58. package/dist/files/editor/code-editor.js +2 -3
  59. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  60. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  61. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  62. package/dist/files/project/components/change-request-status-badge.js +27 -0
  63. package/dist/files/project/components/change-requests-panel.js +1 -1
  64. package/dist/files/project/components/drive-explorer.js +2 -1
  65. package/dist/files/project/components/drive-grid-view.js +11 -7
  66. package/dist/files/project/components/drive-list-view.js +4 -4
  67. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  68. package/dist/files/project/components/file-explorer-page.js +1 -1
  69. package/dist/files/project/components/file-history-popover.js +1 -6
  70. package/dist/files/project/components/file-search.js +6 -1
  71. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  72. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  73. package/dist/files/project/components/version-selector-logic.js +26 -0
  74. package/dist/files/project/components/version-selector.js +11 -20
  75. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  76. package/dist/files/project/hooks/use-change-requests.js +44 -25
  77. package/dist/files/project/hooks/use-server-health.js +17 -15
  78. package/dist/files/renderers/docx/docx-viewer.js +4 -6
  79. package/dist/files/renderers/image-renderer.js +6 -24
  80. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  81. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  82. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  83. package/dist/files/renderers/pdf/pdf-viewer.js +16 -7
  84. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  85. package/dist/files/renderers/show-content-renderer.js +1 -1
  86. package/dist/files/renderers/sqlite-renderer.js +8 -3
  87. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  88. package/dist/files/tree/components/file-tree-item.js +1 -1
  89. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  90. package/dist/files/viewer/file-content-renderer.js +2 -2
  91. package/dist/files/viewer/file-preview-modal.js +1 -1
  92. package/dist/files/viewer/use-file-content-state.js +1 -1
  93. package/dist/i18n/en.d.ts +36 -35
  94. package/dist/i18n/en.js +45 -44
  95. package/dist/index.d.ts +1 -1
  96. package/dist/internal/desktop/desktop-back-button.js +5 -3
  97. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  98. package/dist/internal/hooks/use-notification-center.js +21 -15
  99. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  100. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  101. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  102. package/dist/internal/lib/browser-error-noise.js +1 -1
  103. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  104. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  105. package/dist/internal/lib/provider-branding.js +14 -3
  106. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  107. package/dist/internal/lib/trigger-clock.js +34 -0
  108. package/dist/internal/lib/web-notifications.d.ts +6 -0
  109. package/dist/internal/lib/web-notifications.js +57 -7
  110. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  111. package/dist/internal/notifications/notification-bell.js +1 -1
  112. package/dist/internal/notifications/notification-rows.js +2 -1
  113. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  114. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  115. package/dist/internal/reminders/reminder-filter.js +34 -0
  116. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  117. package/dist/internal/reminders/reminder-format.js +20 -2
  118. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  119. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  120. package/dist/internal/stores/session-hover-store.js +31 -13
  121. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  122. package/dist/markdown/code/inline-code.js +8 -0
  123. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  124. package/dist/markdown/code/output-paths-context.js +35 -0
  125. package/dist/markdown/genui/components/charts.js +6 -5
  126. package/dist/primitives/count-badge.d.ts +3 -1
  127. package/dist/primitives/count-badge.js +3 -3
  128. package/dist/primitives/glyph-paths-source.js +1 -1
  129. package/dist/primitives/glyph-paths.generated.js +1 -1
  130. package/dist/primitives/map.js +2 -1
  131. package/dist/primitives/preview-image.js +4 -2
  132. package/dist/primitives/textarea.js +1 -1
  133. package/dist/primitives/typed-title.js +12 -14
  134. package/dist/primitives/typing-animation.js +4 -2
  135. package/dist/providers/ui-provider.d.ts +57 -0
  136. package/dist/providers/ui-provider.js +4 -0
  137. package/dist/session/composer/ComposerCard.d.ts +2 -0
  138. package/dist/session/composer/ComposerCard.js +5 -1
  139. package/dist/session/composer/agent-selector.js +17 -5
  140. package/dist/session/composer/composer-chat-input.js +5 -3
  141. package/dist/session/composer/composer-logic.js +8 -7
  142. package/dist/session/composer/composer-toolbar.js +4 -2
  143. package/dist/session/composer/composer-underbar.js +3 -1
  144. package/dist/session/composer/composer.js +34 -5
  145. package/dist/session/composer/draft/composer-draft.js +4 -4
  146. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  147. package/dist/session/composer/menus/slash-actions.js +20 -0
  148. package/dist/session/composer/menus/slash-items.js +19 -3
  149. package/dist/session/composer/pasted-text.js +3 -2
  150. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  151. package/dist/session/header/reminder-parts.d.ts +294 -0
  152. package/dist/session/header/reminder-parts.js +120 -0
  153. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  154. package/dist/session/header/session-reminders-indicator.js +142 -35
  155. package/dist/session/media/sandbox-url-detector.js +0 -7
  156. package/dist/session/models/model-availability.d.ts +23 -0
  157. package/dist/session/models/model-availability.js +20 -0
  158. package/dist/session/models/model-grouping.js +1 -4
  159. package/dist/session/models/model-selector.js +23 -14
  160. package/dist/session/models/use-model-connection-gate.js +4 -1
  161. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  162. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  163. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  164. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  165. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  166. package/dist/session/overrides/session-overrides-composer.js +5 -1
  167. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  168. package/dist/session/panel/easy/app-preview.js +12 -2
  169. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  170. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  171. package/dist/session/panel/easy/outputs-card.js +2 -1
  172. package/dist/session/panel/easy/panel-card.js +5 -3
  173. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  174. package/dist/session/panel/easy/preview-load.js +15 -0
  175. package/dist/session/panel/easy/step-detail-body.js +2 -7
  176. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  177. package/dist/session/panel/open-session-quick-view.js +2 -1
  178. package/dist/session/panel/panel-fallback.js +8 -4
  179. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  180. package/dist/session/panel/session-panel-provider.js +19 -3
  181. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  182. package/dist/session/panel/session-reminders-panel.js +109 -0
  183. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  184. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  185. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  186. package/dist/session/panel/use-session-panel-layout.js +7 -5
  187. package/dist/session/print/session-print-header.d.ts +0 -10
  188. package/dist/session/print/session-print-header.js +25 -9
  189. package/dist/session/status/session-surface.js +1 -1
  190. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  191. package/dist/session/thread/optimistic-turn.js +8 -6
  192. package/dist/session/thread/session-chat.d.ts +6 -1
  193. package/dist/session/thread/session-chat.js +34 -12
  194. package/dist/session/thread/session-history.js +3 -1
  195. package/dist/session/thread/session-thread.d.ts +10 -1
  196. package/dist/session/thread/session-thread.js +2 -2
  197. package/dist/session/thread/transcript.js +13 -7
  198. package/dist/session/tool/shared/file-list.js +19 -2
  199. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  200. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  201. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  202. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  203. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  204. package/dist/session/tool/tools/bash-tool.js +10 -6
  205. package/dist/session/tool/tools/web-search-tool.js +25 -3
  206. package/dist/session/turn/burst-step-body.js +4 -0
  207. package/dist/session/turn/recovered-failures.d.ts +8 -0
  208. package/dist/session/turn/recovered-failures.js +32 -0
  209. package/dist/session/turn/streaming-cadence.js +5 -8
  210. package/dist/styles.css +1 -1
  211. package/dist/terminal/session-terminal-panel.js +3 -5
  212. package/dist/theme/theme.js +2 -1
  213. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  214. package/dist/workspace/review/review-center.js +13 -10
  215. package/dist/workspace/session-workspace.js +5 -9
  216. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  217. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  218. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  219. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  220. package/dist/workspace/sidebar/project-session-list.js +52 -55
  221. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  222. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  223. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  224. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  225. package/package.json +7 -3
  226. package/skills/kortix-ui/SKILL.md +1 -1
  227. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  228. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -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: { label: item.label },
518
- actions: [{ label: payload.openSessionLabel ?? openLabel, onClick: () => open(item) }],
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
- : undefined,
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
- sendWebNotification({
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' }
@@ -44,10 +44,11 @@ export function newArrivals(seen, rows) {
44
44
  * is not.
45
45
  */
46
46
  export function planArrivals(fresh, context) {
47
+ const unseen = new Set(context.unseen);
47
48
  const announce = fresh
48
49
  .filter((row) => !row.session_id ||
49
50
  (!context.onScreen(row.session_id) &&
50
- !(row.kind === 'turn_done' && context.unseen.includes(row.session_id))))
51
+ !(row.kind === 'turn_done' && unseen.has(row.session_id))))
51
52
  .slice(0, MAX_ARRIVAL_ALERTS);
52
53
  return context.os ? { toast: [], os: announce } : { toast: announce, os: [] };
53
54
  }
@@ -49,6 +49,11 @@ export interface OutcomeAction {
49
49
  intent: 'open' | 'link';
50
50
  /** Required when `intent` is `link`; ignored otherwise. */
51
51
  href?: string;
52
+ /**
53
+ * For `open`: the source id the modal opens when it is not this outcome's
54
+ * own. A superseded change request opens its replacement (KRTX-3031).
55
+ */
56
+ target?: string;
52
57
  }
53
58
  export interface Outcome {
54
59
  /**
@@ -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 rtf.format(deltaMin, 'minute');
16
+ return relative(deltaMin, 'minute');
15
17
  if (abs < 24 * 60)
16
- return rtf.format(Math.round(deltaMin / 60), 'hour');
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 is instant, so the
29
- * card reads as moving to the row under the pointer instead of closing and
30
- * reopening. This is the rule the design system already states for tooltips:
31
- * delay before the first, none for the rest while the group is active.
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;
@@ -12,6 +12,10 @@ import { create } from 'zustand';
12
12
  * the active row is what is left, and it is also the cheaper shape: exactly one
13
13
  * card is mounted at any time, by construction rather than by timing luck.
14
14
  *
15
+ * The store also holds the active row's element and the one card's content
16
+ * slot (`SessionBriefCardHost`): rows render their brief into that slot, and
17
+ * the host glides to the row instead of a new card appearing at it.
18
+ *
15
19
  * The delays live here rather than on the primitive for a performance reason.
16
20
  * If `openDelay` were a prop derived from `warm`, every row in the sidebar would
17
21
  * re-render each time the group warmed or cooled. Nothing selects `warm`, so it
@@ -21,18 +25,22 @@ import { create } from 'zustand';
21
25
  /** The first card costs a delay, so a pointer crossing the list on its way
22
26
  * somewhere else does not flash open every row it passes over. */
23
27
  export const HOVER_OPEN_DELAY_MS = 200;
28
+ /** While a card is open, a move to another row waits this long, and a newer
29
+ * row restarts the wait: a pointer sweeping down the list moves the card once,
30
+ * to where it stops, instead of re-rendering it for every row it crosses. */
31
+ export const HOVER_MOVE_DEBOUNCE_MS = 50;
24
32
  /** Long enough for the pointer to cross the `sideOffset` gap into the card. */
25
33
  export const HOVER_CLOSE_DELAY_MS = 100;
26
34
  /** How long the group stays warm after its last card closes. */
27
35
  export const HOVER_WARM_GRACE_MS = 300;
28
36
  /**
29
- * Once one card is open the group is warm and the next row is instant, so the
30
- * card reads as moving to the row under the pointer instead of closing and
31
- * reopening. This is the rule the design system already states for tooltips:
32
- * delay before the first, none for the rest while the group is active.
37
+ * Once one card is open the group is warm and the next row costs only the move
38
+ * debounce, so the card reads as moving to the row under the pointer instead of
39
+ * closing and reopening. This is the rule the design system already states for
40
+ * tooltips: delay before the first, (almost) none for the rest.
33
41
  */
34
42
  export function hoverOpenDelayMs(warm) {
35
- return warm ? 0 : HOVER_OPEN_DELAY_MS;
43
+ return warm ? HOVER_MOVE_DEBOUNCE_MS : HOVER_OPEN_DELAY_MS;
36
44
  }
37
45
  /**
38
46
  * Compare-and-clear, the same guard `session-switch-store` uses: a close
@@ -44,6 +52,8 @@ export function clearIfActive(current, sessionId) {
44
52
  let openTimer = null;
45
53
  let closeTimer = null;
46
54
  let warmTimer = null;
55
+ /** The row a pending open will show; its close must also close the card still showing. */
56
+ let pendingId = null;
47
57
  function cancel(timer) {
48
58
  if (timer)
49
59
  clearTimeout(timer);
@@ -51,8 +61,12 @@ function cancel(timer) {
51
61
  }
52
62
  export const useSessionHoverStore = create((set, get) => ({
53
63
  activeSessionId: null,
64
+ anchor: null,
65
+ via: 'pointer',
66
+ slot: null,
54
67
  warm: false,
55
- openSession: (sessionId) => {
68
+ setSlot: (slot) => set({ slot }),
69
+ openSession: (sessionId, anchor, via = 'pointer') => {
56
70
  closeTimer = cancel(closeTimer);
57
71
  warmTimer = cancel(warmTimer);
58
72
  // A pending open belongs to whichever row the pointer has just left.
@@ -61,21 +75,24 @@ export const useSessionHoverStore = create((set, get) => ({
61
75
  // or crossing the gap back from the card would flicker it.
62
76
  if (get().activeSessionId === sessionId)
63
77
  return;
64
- if (hoverOpenDelayMs(get().warm) === 0) {
65
- set({ activeSessionId: sessionId, warm: true });
66
- return;
67
- }
78
+ const show = { activeSessionId: sessionId, anchor: anchor ?? null, via, warm: true };
79
+ pendingId = sessionId;
68
80
  openTimer = setTimeout(() => {
69
81
  openTimer = null;
70
- set({ activeSessionId: sessionId, warm: true });
71
- }, HOVER_OPEN_DELAY_MS);
82
+ pendingId = null;
83
+ set(show);
84
+ }, hoverOpenDelayMs(get().warm));
72
85
  },
73
86
  closeSession: (sessionId) => {
74
87
  openTimer = cancel(openTimer);
75
88
  closeTimer = cancel(closeTimer);
89
+ // Leaving a row whose move had not landed yet: the card still shows the
90
+ // previous row, and that card is the one to close.
91
+ const target = pendingId === sessionId ? get().activeSessionId : sessionId;
92
+ pendingId = null;
76
93
  closeTimer = setTimeout(() => {
77
94
  closeTimer = null;
78
- set((state) => ({ activeSessionId: clearIfActive(state.activeSessionId, sessionId) }));
95
+ set((state) => ({ activeSessionId: target ? clearIfActive(state.activeSessionId, target) : state.activeSessionId }));
79
96
  warmTimer = setTimeout(() => {
80
97
  warmTimer = null;
81
98
  set({ warm: false });
@@ -83,6 +100,7 @@ export const useSessionHoverStore = create((set, get) => ({
83
100
  }, HOVER_CLOSE_DELAY_MS);
84
101
  },
85
102
  dismiss: () => {
103
+ pendingId = null;
86
104
  openTimer = cancel(openTimer);
87
105
  closeTimer = cancel(closeTimer);
88
106
  warmTimer = cancel(warmTimer);
@@ -40,6 +40,8 @@ export function rememberedPanelOf({ terminalOpen, detailKey, }) {
40
40
  return { kind: 'view', view: 'files', target: { changes: true } };
41
41
  if (detailKey === 'audit')
42
42
  return { kind: 'view', view: 'audit' };
43
+ if (detailKey === 'reminders')
44
+ return { kind: 'view', view: 'reminders' };
43
45
  if (detailKey === 'app:landing')
44
46
  return { kind: 'view', view: 'browser' };
45
47
  if (detailKey?.startsWith('app:'))
@@ -12,6 +12,7 @@ import { useSessionBrowserStore } from '../../internal/stores/session-browser-st
12
12
  import { useUiLink } from '../../providers/ui-provider.js';
13
13
  import { useCallback, useEffect, useRef, useState } from 'react';
14
14
  import { useInsideLink } from './inside-link-context.js';
15
+ import { outputPathForName, useOutputPaths } from './output-paths-context.js';
15
16
  import { childrenToText } from './children-text.js';
16
17
  // Server-safe by design — see `inline-chip.tsx`. Re-exported so the client
17
18
  // consumers that already import them from here keep working.
@@ -114,6 +115,7 @@ export function ClickableInlineCode({ children }) {
114
115
  const { proxyUrl } = useSandboxProxy();
115
116
  const openPreview = useOpenPreviewLink();
116
117
  const insideLink = useInsideLink();
118
+ const outputPaths = useOutputPaths();
117
119
  const text = childrenToText(children).trim();
118
120
  // Before the URL and path checks: a hex is neither, and the test is a single
119
121
  // anchored regex against a string that is at most nine characters.
@@ -146,5 +148,11 @@ export function ClickableInlineCode({ children }) {
146
148
  if (looksLikeFilePath(text)) {
147
149
  return _jsx(FilePathCode, { text: text, children: children });
148
150
  }
151
+ // A bare filename (`claims.md`) is no path, but it may name one of this
152
+ // session's outputs: open that file, still showing the name.
153
+ const outputPath = outputPathForName(text, outputPaths);
154
+ if (outputPath) {
155
+ return _jsx(FilePathCode, { text: outputPath, children: children });
156
+ }
149
157
  return _jsx("code", { className: INLINE_CODE, children: children });
150
158
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The paths of the files this session produced: its Outputs.
3
+ *
4
+ * A reply names its deliverables by bare filename (`claims.md`), and a bare
5
+ * name is not a path, so it never linked. Opening it took Expand pane →
6
+ * Outputs → the file (KRTX-3023). Inline code that names exactly one of these
7
+ * outputs now opens it.
8
+ *
9
+ * Provider: `SessionPanelProvider`. Empty outside a session: nothing extra links.
10
+ */
11
+ export declare const OutputPathsContext: import("react").Context<readonly string[]>;
12
+ export declare const useOutputPaths: () => readonly string[];
13
+ /**
14
+ * The output a bare filename names, or null: it is not a filename, no output
15
+ * has that name, or two different outputs share it and the reply does not say
16
+ * which.
17
+ */
18
+ export declare function outputPathForName(text: string, paths: readonly string[]): string | null;
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+ import { createContext, useContext } from 'react';
3
+ /**
4
+ * The paths of the files this session produced: its Outputs.
5
+ *
6
+ * A reply names its deliverables by bare filename (`claims.md`), and a bare
7
+ * name is not a path, so it never linked. Opening it took Expand pane →
8
+ * Outputs → the file (KRTX-3023). Inline code that names exactly one of these
9
+ * outputs now opens it.
10
+ *
11
+ * Provider: `SessionPanelProvider`. Empty outside a session: nothing extra links.
12
+ */
13
+ export const OutputPathsContext = createContext([]);
14
+ export const useOutputPaths = () => useContext(OutputPathsContext);
15
+ const BARE_FILENAME = /^[\w@][\w@.-]*\.[A-Za-z0-9]+$/;
16
+ /** `/workspace/a/b.md` and `a/b.md` are one file. */
17
+ const sameFileKey = (path) => path.replace(/^\/workspace\//, '');
18
+ /**
19
+ * The output a bare filename names, or null: it is not a filename, no output
20
+ * has that name, or two different outputs share it and the reply does not say
21
+ * which.
22
+ */
23
+ export function outputPathForName(text, paths) {
24
+ if (!BARE_FILENAME.test(text))
25
+ return null;
26
+ let found = null;
27
+ for (const path of paths) {
28
+ if (path.slice(path.lastIndexOf('/') + 1) !== text)
29
+ continue;
30
+ if (found !== null && sameFileKey(found) !== sameFileKey(path))
31
+ return null;
32
+ found ??= path;
33
+ }
34
+ return found;
35
+ }
@@ -25,7 +25,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
25
25
  */
26
26
  // eslint-disable-next-line no-restricted-imports -- screen-reader text for the figure; this module is reached only through lazy()
27
27
  import { genuiA11yText } from '@kortix/sdk/genui';
28
- import { useState } from 'react';
28
+ import { useMemo, useState } from 'react';
29
29
  import { Bar, BarChart, CartesianGrid, Cell, Line, LineChart, Pie, PieChart, XAxis, YAxis } from 'recharts';
30
30
  import { ChartContainer, ChartTooltip, ChartTooltipContent } from '../../../primitives/chart.js';
31
31
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../../../primitives/dialog.js';
@@ -95,7 +95,7 @@ function PieFigure({ variant, slices, total, unit, format, tooltip, focus, }) {
95
95
  const restTotal = rest.reduce((sum, slice) => sum + slice.value, 0);
96
96
  const block = (slice, i) => {
97
97
  const small = i > 0 && slice.value / restTotal < 0.12;
98
- return (_jsxs(HoverCard, { openDelay: 80, closeDelay: 40, children: [_jsx(HoverCardTrigger, { asChild: true, children: _jsx("div", { tabIndex: 0, onMouseEnter: () => focus.hover(slice.key), onMouseLeave: () => focus.hover(null), onClick: () => focus.toggle(slice.key), "aria-label": `${slice.label}: ${format(slice.value)} (${slice.percent}%)`, className: cn('focus-visible:ring-ring duration-fast flex min-h-1.5 min-w-0 flex-col justify-between overflow-hidden rounded-md transition-opacity outline-none focus-visible:ring-2', small ? 'p-0' : 'p-3', i < 2 ? 'text-background' : 'text-foreground'), style: { backgroundColor: PIE_RAMP[i], flexGrow: slice.value, flexBasis: 0, opacity: dimmed(focus, slice.key) }, children: small ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate text-xs", children: slice.label }), _jsxs("span", { className: cn('truncate tabular-nums', i === 0 ? 'text-2xl font-medium' : 'text-sm'), children: [slice.percent, "%"] })] })) }) }), _jsxs(HoverCardContent, { side: "top", className: "flex w-auto min-w-40 flex-col gap-1 p-3", children: [_jsxs("span", { className: "text-foreground flex items-center gap-2 text-sm font-medium", children: [dot(slice.fill), slice.label] }), _jsxs("span", { className: "text-muted-foreground font-mono text-xs tabular-nums", children: [unit ? `${format(slice.value)} ${unit}` : format(slice.value), " \u00B7 ", slice.percent, "%"] })] })] }, slice.key));
98
+ return (_jsxs(HoverCard, { openDelay: 80, closeDelay: 40, children: [_jsx(HoverCardTrigger, { asChild: true, children: _jsx("button", { type: "button", onMouseEnter: () => focus.hover(slice.key), onMouseLeave: () => focus.hover(null), onClick: () => focus.toggle(slice.key), "aria-label": `${slice.label}: ${format(slice.value)} (${slice.percent}%)`, className: cn('focus-visible:ring-ring duration-fast flex min-h-1.5 min-w-0 flex-col justify-between overflow-hidden rounded-md text-left transition-opacity outline-none focus-visible:ring-2', small ? 'p-0' : 'p-3', i < 2 ? 'text-background' : 'text-foreground'), style: { backgroundColor: PIE_RAMP[i], flexGrow: slice.value, flexBasis: 0, opacity: dimmed(focus, slice.key) }, children: small ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate text-xs", children: slice.label }), _jsxs("span", { className: cn('truncate tabular-nums', i === 0 ? 'text-2xl font-medium' : 'text-sm'), children: [slice.percent, "%"] })] })) }) }), _jsxs(HoverCardContent, { side: "top", className: "flex w-auto min-w-40 flex-col gap-1 p-3", children: [_jsxs("span", { className: "text-foreground flex items-center gap-2 text-sm font-medium", children: [dot(slice.fill), slice.label] }), _jsxs("span", { className: "text-muted-foreground font-mono text-xs tabular-nums", children: [unit ? `${format(slice.value)} ${unit}` : format(slice.value), " \u00B7 ", slice.percent, "%"] })] })] }, slice.key));
99
99
  };
100
100
  return (_jsxs("div", { className: "flex min-h-0 flex-1 gap-1", children: [block(first, 0), rest.length > 0 ? (_jsx("div", { className: "flex min-w-0 flex-col gap-1", style: { flexGrow: restTotal, flexBasis: 0 }, children: rest.map((slice, i) => block(slice, i + 1)) })) : null] }));
101
101
  }
@@ -118,11 +118,11 @@ export function ChartView({ node, props }) {
118
118
  const t = useTranslations('genui');
119
119
  const focus = useFocus();
120
120
  const locale = useLocale();
121
+ const number = useMemo(() => new Intl.NumberFormat(locale), [locale]);
122
+ const compact = useMemo(() => new Intl.NumberFormat(locale, { notation: 'compact' }), [locale]);
121
123
  // A streamed pie arrives before its slices resolve (the parent line comes first). Hold its space until one lands.
122
124
  if (node.type === 'PieChart' && kids(props.slices).length === 0)
123
125
  return GenuiPending(node);
124
- const number = new Intl.NumberFormat(locale);
125
- const compact = new Intl.NumberFormat(locale, { notation: 'compact' });
126
126
  const format = (value) => (typeof value === 'number' ? number.format(value) : value == null ? '—' : String(value));
127
127
  // The tooltip row of `ChartTooltipContent`, with the table's number format.
128
128
  const tooltip = (_jsx(ChartTooltipContent, { hideLabel: node.type === 'PieChart', formatter: (value, name, item) => (_jsxs(_Fragment, { children: [_jsx("span", { className: "size-2.5 shrink-0 rounded-xs", style: { backgroundColor: item.payload?.fill ?? item.color } }), _jsxs("span", { className: "flex flex-1 items-center justify-between gap-2 leading-none", children: [_jsx("span", { className: "text-muted-foreground", children: name }), _jsx("span", { className: "text-foreground font-mono font-medium tabular-nums", children: format(value) })] })] })) }));
@@ -160,7 +160,8 @@ export function ChartView({ node, props }) {
160
160
  /** The chart's values as text: the Show data dialog's table. A missing value stays a dash, never a zero. */
161
161
  export function ChartDataTable({ node }) {
162
162
  const t = useTranslations('genui');
163
- const number = new Intl.NumberFormat(useLocale());
163
+ const locale = useLocale();
164
+ const number = useMemo(() => new Intl.NumberFormat(locale), [locale]);
164
165
  const format = (value) => (typeof value === 'number' ? number.format(value) : value == null ? '—' : String(value));
165
166
  const { props } = node;
166
167
  let head;
@@ -19,8 +19,10 @@ declare const TONE: {
19
19
  readonly conflict: "bg-kortix-orange/15";
20
20
  };
21
21
  export type CountBadgeTone = keyof typeof TONE;
22
- export declare function CountBadge({ count, tone, className, ...props }: {
22
+ export declare function CountBadge({ count, tone, icon, className, ...props }: {
23
23
  count: number;
24
24
  tone?: CountBadgeTone;
25
+ /** A glyph that names what is counted, drawn before the number. */
26
+ icon?: React.ReactNode;
25
27
  } & Omit<React.ComponentProps<'span'>, 'children'>): import("react").JSX.Element;
26
28
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Badge } from './badge.js';
3
3
  import { cn } from '../lib/utils.js';
4
4
  /**
@@ -21,6 +21,6 @@ const TONE = {
21
21
  reminder: 'bg-kortix-purple/15',
22
22
  conflict: 'bg-kortix-orange/15',
23
23
  };
24
- export function CountBadge({ count, tone = 'neutral', className, ...props }) {
25
- return (_jsx(Badge, { variant: "transparent", size: "tabular", className: cn(TONE[tone], 'shrink-0 text-current', className), ...props, children: count > 99 ? '99+' : count }));
24
+ export function CountBadge({ count, tone = 'neutral', icon, className, ...props }) {
25
+ return (_jsxs(Badge, { variant: "transparent", size: "tabular", className: cn(TONE[tone], 'shrink-0 text-current', icon && 'gap-1', className), ...props, children: [icon, count > 99 ? '99+' : count] }));
26
26
  }
@@ -27,7 +27,7 @@ export function renderGlyphPathsModule() {
27
27
  const paths = glyphPathsInDefaultWeight();
28
28
  const lines = Object.entries(paths).map(([name, d]) => ` ${JSON.stringify(name)}: ${JSON.stringify(d)},`);
29
29
  return [
30
- '// GENERATED by apps/web/scripts/generate-glyph-paths.tsx. Do not edit.',
30
+ '// GENERATED by packages/ui/scripts/generate-glyph-paths.tsx. Do not edit.',
31
31
  '// The path of every catalogue glyph in DEFAULT_ICON_WEIGHT; see glyph-face.tsx.',
32
32
  '',
33
33
  `export const GLYPH_PATHS_WEIGHT = ${JSON.stringify(DEFAULT_ICON_WEIGHT)};`,
@@ -1,4 +1,4 @@
1
- // GENERATED by apps/web/scripts/generate-glyph-paths.tsx. Do not edit.
1
+ // GENERATED by packages/ui/scripts/generate-glyph-paths.tsx. Do not edit.
2
2
  // The path of every catalogue glyph in DEFAULT_ICON_WEIGHT; see glyph-face.tsx.
3
3
  export const GLYPH_PATHS_WEIGHT = "bold";
4
4
  export const GLYPH_PATHS = {