@kortix/ui 0.14.5 → 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 (124) hide show
  1. package/dist/access/access/access-dialog.js +8 -5
  2. package/dist/access/access/copy-row.js +2 -2
  3. package/dist/access/sharing-picker.js +2 -1
  4. package/dist/browser/app-in-browser.js +5 -3
  5. package/dist/browser/browser-panel.js +6 -6
  6. package/dist/browser/sandbox-browser-chrome.d.ts +4 -1
  7. package/dist/browser/sandbox-browser-chrome.js +4 -3
  8. package/dist/customize/agents/agent-access.d.ts +20 -0
  9. package/dist/customize/agents/agent-access.js +17 -0
  10. package/dist/customize/agents/agent-grant-pages.js +5 -6
  11. package/dist/customize/agents/agent-page.js +2 -1
  12. package/dist/customize/agents/agents-page.js +18 -1
  13. package/dist/customize/connectors/add-mcp/add-mcp-server.js +4 -2
  14. package/dist/customize/connectors/connector-routes.d.ts +3 -1
  15. package/dist/customize/connectors/connector-routes.js +18 -0
  16. package/dist/customize/connectors/connectors-page.js +23 -4
  17. package/dist/customize/connectors/detail/connector-advanced-tools.js +4 -6
  18. package/dist/customize/providers/provider-accordion.js +2 -1
  19. package/dist/customize/providers/provider-connect.js +2 -1
  20. package/dist/customize/sections/component/telegram-channel-row.js +1 -1
  21. package/dist/customize/sections/connector-connection-modal.js +11 -9
  22. package/dist/customize/sections/connectors-view.js +11 -13
  23. package/dist/customize/sections/view/gateway/gateway-routing.js +15 -18
  24. package/dist/customize/settings/rail-copy-context.js +5 -3
  25. package/dist/customize/settings/tabs/sandbox-template-form.js +9 -12
  26. package/dist/customize/shared/capability-tabs.js +2 -1
  27. package/dist/customize/skills/skills-page.js +3 -5
  28. package/dist/customize/triggers/schedule/event-trigger-copy.js +3 -2
  29. package/dist/customize/triggers/schedule/schedule-builder.js +7 -4
  30. package/dist/customize/triggers/schedule/trigger-composer.js +5 -2
  31. package/dist/customize/triggers/schedule/when-event.js +1 -1
  32. package/dist/diff/diff-view.js +9 -10
  33. package/dist/files/editor/code-editor.js +2 -3
  34. package/dist/files/project/components/change-request-detail-dialog.d.ts +3 -1
  35. package/dist/files/project/components/change-request-detail-dialog.js +3 -5
  36. package/dist/files/project/components/change-request-status-badge.d.ts +12 -0
  37. package/dist/files/project/components/change-request-status-badge.js +27 -0
  38. package/dist/files/project/components/change-requests-panel.js +1 -1
  39. package/dist/files/project/components/drive-explorer.js +2 -1
  40. package/dist/files/project/components/drive-grid-view.js +11 -7
  41. package/dist/files/project/components/drive-list-view.js +4 -4
  42. package/dist/files/project/components/file-breadcrumbs.js +1 -1
  43. package/dist/files/project/components/file-explorer-page.js +1 -1
  44. package/dist/files/project/components/file-history-popover.js +1 -6
  45. package/dist/files/project/components/file-search.js +6 -1
  46. package/dist/files/project/components/open-change-request-dialog.js +2 -1
  47. package/dist/files/project/components/version-selector-logic.d.ts +11 -0
  48. package/dist/files/project/components/version-selector-logic.js +26 -0
  49. package/dist/files/project/components/version-selector.js +11 -20
  50. package/dist/files/project/hooks/use-server-health.js +17 -15
  51. package/dist/files/renderers/docx/docx-viewer.js +0 -5
  52. package/dist/files/renderers/image-renderer.js +6 -24
  53. package/dist/files/renderers/pdf/pdf-viewer.js +1 -1
  54. package/dist/files/renderers/presentation/FullScreenPresentationViewer.js +2 -11
  55. package/dist/files/renderers/show-content-renderer.js +1 -1
  56. package/dist/files/renderers/sqlite-renderer.js +8 -3
  57. package/dist/files/renderers/xlsx/xlsx-viewer.js +15 -16
  58. package/dist/files/tree/components/file-tree-item.js +1 -1
  59. package/dist/files/viewer/file-preview-modal.js +1 -1
  60. package/dist/files/viewer/use-file-content-state.js +1 -1
  61. package/dist/i18n/en.d.ts +10 -0
  62. package/dist/i18n/en.js +10 -0
  63. package/dist/internal/desktop/desktop-back-button.js +5 -3
  64. package/dist/internal/hooks/use-auto-scroll.js +5 -3
  65. package/dist/internal/notifications/notification-rows.js +2 -1
  66. package/dist/internal/outcomes/outcome-types.d.ts +5 -0
  67. package/dist/markdown/code/inline-code.js +8 -0
  68. package/dist/markdown/code/output-paths-context.d.ts +18 -0
  69. package/dist/markdown/code/output-paths-context.js +35 -0
  70. package/dist/markdown/genui/components/charts.js +6 -5
  71. package/dist/primitives/map.js +2 -1
  72. package/dist/primitives/preview-image.js +4 -2
  73. package/dist/primitives/textarea.js +1 -1
  74. package/dist/primitives/typed-title.js +12 -14
  75. package/dist/primitives/typing-animation.js +4 -2
  76. package/dist/session/composer/agent-selector.js +6 -1
  77. package/dist/session/composer/composer-chat-input.js +5 -3
  78. package/dist/session/composer/composer-logic.js +8 -7
  79. package/dist/session/composer/draft/composer-draft.js +4 -4
  80. package/dist/session/composer/pasted-text.js +3 -2
  81. package/dist/session/composer/stores/queued-draft-store.js +3 -2
  82. package/dist/session/header/reminder-parts.d.ts +11 -2
  83. package/dist/session/header/reminder-parts.js +9 -4
  84. package/dist/session/media/sandbox-url-detector.js +0 -7
  85. package/dist/session/outcomes/change-request-outcomes.js +24 -3
  86. package/dist/session/overrides/provider-secret-pool-editor.js +2 -1
  87. package/dist/session/panel/advanced/advanced-panel.js +3 -7
  88. package/dist/session/panel/easy/app-preview.js +12 -2
  89. package/dist/session/panel/easy/easy-panel-logic.d.ts +1 -1
  90. package/dist/session/panel/easy/easy-panel-logic.js +1 -1
  91. package/dist/session/panel/easy/outputs-card.js +2 -1
  92. package/dist/session/panel/easy/panel-card.js +5 -3
  93. package/dist/session/panel/easy/preview-load.d.ts +14 -0
  94. package/dist/session/panel/easy/preview-load.js +15 -0
  95. package/dist/session/panel/easy/step-detail-body.js +2 -7
  96. package/dist/session/panel/session-panel-provider.js +6 -2
  97. package/dist/session/panel/shared/use-panel-derivations.js +6 -4
  98. package/dist/session/panel/use-session-panel-layout.js +7 -5
  99. package/dist/session/print/session-print-header.js +9 -8
  100. package/dist/session/thread/session-chat.js +12 -9
  101. package/dist/session/thread/session-history.js +3 -1
  102. package/dist/session/thread/transcript.js +13 -7
  103. package/dist/session/tool/shared/file-list.js +19 -2
  104. package/dist/session/tool/shared/infrastructure-contexts.d.ts +7 -0
  105. package/dist/session/tool/shared/infrastructure-contexts.js +7 -0
  106. package/dist/session/tool/shared/infrastructure-output.js +9 -5
  107. package/dist/session/tool/shared/infrastructure-row-header.js +9 -1
  108. package/dist/session/tool/shared/infrastructure-rows.js +7 -5
  109. package/dist/session/tool/tools/bash-tool.js +10 -6
  110. package/dist/session/tool/tools/web-search-tool.js +25 -3
  111. package/dist/session/turn/recovered-failures.d.ts +8 -0
  112. package/dist/session/turn/recovered-failures.js +32 -0
  113. package/dist/session/turn/streaming-cadence.js +5 -8
  114. package/dist/styles.css +1 -1
  115. package/dist/terminal/session-terminal-panel.js +3 -5
  116. package/dist/theme/theme.js +2 -1
  117. package/dist/workspace/projects/modal/github-import-pickers.js +7 -6
  118. package/dist/workspace/review/review-center.js +13 -10
  119. package/dist/workspace/session-workspace.js +5 -9
  120. package/dist/workspace/sessions/deleted-session-row.js +5 -2
  121. package/dist/workspace/sidebar/footer/project-computer-nav.js +6 -4
  122. package/dist/workspace/sidebar/footer/project-mcp-nav.js +6 -2
  123. package/dist/workspace/sidebar/modal/rename-session-modal.js +8 -3
  124. package/package.json +3 -3
package/dist/i18n/en.d.ts CHANGED
@@ -4068,6 +4068,8 @@ declare const catalog: {
4068
4068
  texta4cf35d7f3ca: string;
4069
4069
  texta4f264c3ab5f: string;
4070
4070
  texta4fc39798651: string;
4071
+ textddcd87c21894: string;
4072
+ texta8ef31fa7d25: string;
4071
4073
  texta514a684676a: string;
4072
4074
  texta52ace420f21: string;
4073
4075
  texta54220c82b33: string;
@@ -4286,6 +4288,11 @@ declare const catalog: {
4286
4288
  textb6b7a0065808: string;
4287
4289
  textb6bb289a7a2c: string;
4288
4290
  textb6c70926c1aa: string;
4291
+ textf19611c61ca5: string;
4292
+ text15589a542412: string;
4293
+ texte96d99ad9896: string;
4294
+ text70b8d7e10749: string;
4295
+ text2d3f4433505a: string;
4289
4296
  textb6cde9030cef: string;
4290
4297
  textb6d85433a7ee: string;
4291
4298
  textb70354a1faae: string;
@@ -4375,6 +4382,7 @@ declare const catalog: {
4375
4382
  textbdb86505f806: string;
4376
4383
  textbdbd7706de64: string;
4377
4384
  textbdc0857b99a9: string;
4385
+ textbdc090ec61e3: string;
4378
4386
  textbdc7d54433e6: string;
4379
4387
  textbde05a7b8929: string;
4380
4388
  textbe379a30f1aa: string;
@@ -6265,6 +6273,8 @@ declare const catalog: {
6265
6273
  timeZone: string;
6266
6274
  lastRun: string;
6267
6275
  lastRunFailed: string;
6276
+ runQueued: string;
6277
+ runFailed: string;
6268
6278
  neverRun: string;
6269
6279
  searchPlaceholder: string;
6270
6280
  statusAll: string;
package/dist/i18n/en.js CHANGED
@@ -4069,6 +4069,8 @@ const catalog = {
4069
4069
  "texta4cf35d7f3ca": "Provision a few sandboxes — their timing and outcome show up here.",
4070
4070
  "texta4f264c3ab5f": "Admin Grant",
4071
4071
  "texta4fc39798651": "Change request #{number}",
4072
+ "textddcd87c21894": "Superseded by #{number}",
4073
+ "texta8ef31fa7d25": "View #{number}",
4072
4074
  "texta514a684676a": "Edit access",
4073
4075
  "texta52ace420f21": "All",
4074
4076
  "texta54220c82b33": "Optional at session start. Click to require this connector.",
@@ -4287,6 +4289,11 @@ const catalog = {
4287
4289
  "textb6b7a0065808": "Repeat",
4288
4290
  "textb6bb289a7a2c": "accounts teams organizations switch manage",
4289
4291
  "textb6c70926c1aa": "Admins only",
4292
+ "textf19611c61ca5": "Full access",
4293
+ "text15589a542412": "{count, plural, =0 {No connectors} one {# connector} other {# connectors}}",
4294
+ "texte96d99ad9896": "{count, plural, =0 {No secrets} one {# secret} other {# secrets}}",
4295
+ "text70b8d7e10749": "All Kortix actions",
4296
+ "text2d3f4433505a": "{count, plural, =0 {No Kortix actions} one {# Kortix action} other {# Kortix actions}}",
4290
4297
  "textb6cde9030cef": "Create key",
4291
4298
  "textb6d85433a7ee": "Sessions could not be loaded",
4292
4299
  "textb70354a1faae": "Foo 7B",
@@ -4376,6 +4383,7 @@ const catalog = {
4376
4383
  "textbdb86505f806": "Archived",
4377
4384
  "textbdbd7706de64": "How this works",
4378
4385
  "textbdc0857b99a9": "Only me",
4386
+ "textbdc090ec61e3": "Reload",
4379
4387
  "textbdc7d54433e6": "Failed to add connector",
4380
4388
  "textbde05a7b8929": "Checking session",
4381
4389
  "textbe379a30f1aa": "credits",
@@ -6266,6 +6274,8 @@ const catalog = {
6266
6274
  "timeZone": "Time zone",
6267
6275
  "lastRun": "Last run",
6268
6276
  "lastRunFailed": "Last run failed",
6277
+ "runQueued": "Reminder sent to the session",
6278
+ "runFailed": "Could not run the reminder",
6269
6279
  "neverRun": "Not run yet",
6270
6280
  "searchPlaceholder": "Search reminders",
6271
6281
  "statusAll": "All",
@@ -87,6 +87,8 @@ export function useDesktopBackTarget(href) {
87
87
  };
88
88
  }, [href]);
89
89
  }
90
+ const subscribeNoop = () => () => { };
91
+ const readCanGoBack = () => Boolean(window.navigation?.canGoBack);
90
92
  /** Pathname of an app href, for comparing against `useUiPathname()`. */
91
93
  function hrefPath(href) {
92
94
  return href.split(/[?#]/)[0] || '/';
@@ -119,10 +121,10 @@ export function DesktopBackButton() {
119
121
  const pathname = useUiPathname() ?? '/';
120
122
  const home = useAppHome();
121
123
  const to = useSyncExternalStore(subscribeBackTarget, () => backTarget, () => undefined);
124
+ const canGoBack = useSyncExternalStore(subscribeNoop, readCanGoBack, () => false);
122
125
  if (!viewerId)
123
126
  return null;
124
- const win = window;
125
- if (!hasBackDestination(win, { to, home, pathname }))
127
+ if (!hasBackDestination({ navigation: { canGoBack } }, { to, home, pathname }))
126
128
  return null;
127
- return _jsx(DesktopBackControl, { onBack: () => void goBack(router, win, { to, home }) });
129
+ return _jsx(DesktopBackControl, { onBack: () => void goBack(router, window, { to, home }) });
128
130
  }
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { OWN_SCROLL_MS, chevronVisible, isAtEnd, keyScrollIntentFor, pickAnchorIndex, prefersReducedMotion, roomUnderNewestTurn, settleMotion, shouldReleaseFollow, } from './use-auto-scroll-physics.js';
4
4
  export { AT_END_PX, BOTTOM_GAP_PX, CHEVRON_PX, GLIDE_MIN_PX, OWN_SCROLL_MS, TURN_TOP_OFFSET, chevronVisible, classifyScrollKey, isAtEnd, isEditableTarget, isSpaceActivatedTarget, keyScrollIntentFor, pickAnchorIndex, roomUnderNewestTurn, settleMotion, shouldReleaseFollow, } from './use-auto-scroll-physics.js';
5
5
  /**
@@ -234,7 +234,9 @@ export function useAutoScroll({ hasContent = false, } = {}) {
234
234
  markOwnScroll(OWN_SCROLL_MS);
235
235
  el.scrollTop = end;
236
236
  }, [sizeRoom, glideTo, markOwnScroll]);
237
- settleRef.current = settle;
237
+ useLayoutEffect(() => {
238
+ settleRef.current = settle;
239
+ }, [settle]);
238
240
  const goToEnd = useCallback((behavior) => {
239
241
  const el = scrollRef.current;
240
242
  if (!el)
@@ -455,7 +457,7 @@ export function useAutoScroll({ hasContent = false, } = {}) {
455
457
  document.removeEventListener('keydown', onKeyDown, { capture: true });
456
458
  el.removeEventListener('scroll', onScroll);
457
459
  };
458
- }, [goToEnd, endGlide, hasContent, setFollow, markOwnScroll]);
460
+ }, [goToEnd, endGlide, hasContent, setFollow]);
459
461
  return {
460
462
  scrollRef,
461
463
  contentRef,
@@ -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
  /**
@@ -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;
@@ -163,7 +163,8 @@ function MapMarker({ longitude, latitude, label, children, ...markerOptions }) {
163
163
  marker.setLngLat([longitude, latitude]);
164
164
  }
165
165
  }, [marker, longitude, latitude]);
166
- return _jsx(MarkerContext.Provider, { value: { marker, map }, children: children });
166
+ const markerContextValue = useMemo(() => ({ marker, map }), [marker, map]);
167
+ return _jsx(MarkerContext.Provider, { value: markerContextValue, children: children });
167
168
  }
168
169
  function MarkerContent({ children, className }) {
169
170
  const { marker } = useMarkerContext();
@@ -22,10 +22,12 @@ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
22
22
  const PreviewImageContent = React.forwardRef(({ className, fileContent, fileName, ...props }, ref) => {
23
23
  const [zoomLevel, setZoomLevel] = React.useState(DEFAULT_ZOOM);
24
24
  const [transformOrigin, setTransformOrigin] = React.useState(DEFAULT_ORIGIN);
25
- React.useEffect(() => {
25
+ const [zoomedContent, setZoomedContent] = React.useState(fileContent);
26
+ if (fileContent !== zoomedContent) {
27
+ setZoomedContent(fileContent);
26
28
  setZoomLevel(DEFAULT_ZOOM);
27
29
  setTransformOrigin(DEFAULT_ORIGIN);
28
- }, [fileContent]);
30
+ }
29
31
  const getTransformOrigin = (event) => {
30
32
  const rect = event.currentTarget.getBoundingClientRect();
31
33
  if (!rect.width || !rect.height) {
@@ -59,7 +59,7 @@ export const useAutosizeTextArea = ({ textAreaRef, triggerAutoSize, maxHeight =
59
59
  clearTimeout(resizeTimeoutRef.current);
60
60
  }
61
61
  };
62
- }, [textAreaRef.current, triggerAutoSize, minHeight, maxHeight, currentHeight, init]);
62
+ }, [textAreaRef, triggerAutoSize, minHeight, maxHeight, currentHeight, init]);
63
63
  };
64
64
  export const Textarea = React.forwardRef(({ maxHeight = Number.MAX_SAFE_INTEGER, minHeight = 52, className, onChange, value, variant: _variant, ...props }, ref) => {
65
65
  const textAreaRef = React.useRef(null);
@@ -19,25 +19,23 @@ import { shouldTypeOnChange, TYPE_SPEED_MS } from './typed-title-logic.js';
19
19
  import { TypingAnimation } from './typing-animation.js';
20
20
  import { cn } from '../lib/utils.js';
21
21
  import { useReducedMotion } from 'motion/react';
22
- import { useCallback, useEffect, useRef, useState } from 'react';
22
+ import { useCallback, useState } from 'react';
23
23
  export { TYPE_SPEED_MS };
24
24
  export function useTypeOnChange(text) {
25
- // The last value this row DISPLAYED, in a ref rather than state: it must not
26
- // itself cause a render, and it has to update in the same pass that decides
27
- // to animate, or a second change arriving mid-type compares against a stale
28
- // value and is missed.
29
- const previousRef = useRef(null);
25
+ // The last value this row DISPLAYED. It updates in the same render pass that
26
+ // decides to animate, or a second change arriving mid-type compares against
27
+ // a stale value and is missed.
28
+ const [previous, setPrevious] = useState(null);
30
29
  const [isTyping, setIsTyping] = useState(false);
31
30
  const [runId, setRunId] = useState(0);
32
31
  const reduceMotion = !!useReducedMotion();
33
- useEffect(() => {
34
- const previous = previousRef.current;
35
- previousRef.current = text;
36
- if (!shouldTypeOnChange({ previous, next: text, reduceMotion }))
37
- return;
38
- setIsTyping(true);
39
- setRunId((id) => id + 1);
40
- }, [text, reduceMotion]);
32
+ if (previous !== text) {
33
+ setPrevious(text);
34
+ if (shouldTypeOnChange({ previous, next: text, reduceMotion })) {
35
+ setIsTyping(true);
36
+ setRunId((id) => id + 1);
37
+ }
38
+ }
41
39
  const stop = useCallback(() => setIsTyping(false), []);
42
40
  return { isTyping, runId, stop };
43
41
  }
@@ -34,12 +34,14 @@ export function TypingAnimation({ children, words, className, duration = 100, ty
34
34
  const deletingSpeed = deleteSpeed ?? typingSpeed / 2;
35
35
  const shouldStart = startOnView ? isInView : true;
36
36
  const animationSourceKey = useMemo(() => (words ? words.join('\u0000') : (children ?? '')), [words, children]);
37
- useEffect(() => {
37
+ const [seenSourceKey, setSeenSourceKey] = useState(animationSourceKey);
38
+ if (animationSourceKey !== seenSourceKey) {
39
+ setSeenSourceKey(animationSourceKey);
38
40
  setDisplayedText('');
39
41
  setCurrentWordIndex(0);
40
42
  setCurrentCharIndex(0);
41
43
  setPhase('typing');
42
- }, [animationSourceKey]);
44
+ }
43
45
  useEffect(() => {
44
46
  let timeout = null;
45
47
  if (shouldStart && wordsToAnimate.length > 0) {
@@ -45,7 +45,12 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
45
45
  const [search, setSearch] = useState('');
46
46
  const { offeredAgents, agentLabels } = useUiHostConfig();
47
47
  // A host that lists its agents (`offeredAgents`) offers only those; it names them with `agentLabels`.
48
- const offered = useMemo(() => (offeredAgents ? agents.filter((a) => offeredAgents.includes(a.name)) : agents), [agents, offeredAgents]);
48
+ const offered = useMemo(() => {
49
+ if (!offeredAgents)
50
+ return agents;
51
+ const offeredNames = new Set(offeredAgents);
52
+ return agents.filter((a) => offeredNames.has(a.name));
53
+ }, [agents, offeredAgents]);
49
54
  const primaryAgents = useMemo(() => composerSelectableAgents(offered), [offered]);
50
55
  const label = (name) => agentLabels?.[name] ?? (isMetaAgentName(name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(name));
51
56
  // A `flash` state, a `prevAgentRef` feeding it, two effects and a 400ms
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { SessionOverridesComposer } from '../overrides/session-overrides-composer.js';
5
5
  import { SessionChatInput, } from './session-chat-input.js';
6
6
  import { useProjectConfig, useRuntimeAgents, useRuntimeCommands, useRuntimeConfig, useRuntimeProviders, useSessionModelSelection } from '@kortix/sdk/react';
@@ -103,10 +103,11 @@ export function ComposerChatInput({ onSend, onCommand, sessionId, projectId, isB
103
103
  useEffect(() => {
104
104
  onAgentSelectionChange?.(selectedAgentName);
105
105
  }, [onAgentSelectionChange, selectedAgentName]);
106
- const [newSessionScope, setNewSessionScope] = useState(null);
106
+ // Read only by `options()` at send time, so a ref: a commit needs no render.
107
+ const newSessionScopeRef = useRef(null);
107
108
  const [newProviderSecretPools, setNewProviderSecretPools] = useState({});
108
109
  const handleCommittedScope = useCallback((commit) => {
109
- setNewSessionScope(commit ? { agentName: selectedAgentName, commit } : null);
110
+ newSessionScopeRef.current = commit ? { agentName: selectedAgentName, commit } : null;
110
111
  }, [selectedAgentName]);
111
112
  // The axes a session can override that have no control of their own —
112
113
  // secrets, connectors, sandbox. Agent, model and effort are NOT passed: this
@@ -126,6 +127,7 @@ export function ComposerChatInput({ onSend, onCommand, sessionId, projectId, isB
126
127
  o.model = local.model.currentKey;
127
128
  if (local.model.variant.current)
128
129
  o.variant = local.model.variant.current;
130
+ const newSessionScope = newSessionScopeRef.current;
129
131
  if (!sessionId && newSessionScope && newSessionScope.agentName === selectedAgentName) {
130
132
  o.scope = newSessionScope.commit;
131
133
  }
@@ -51,11 +51,7 @@ export function extractReplyQuotes(text) {
51
51
  const { quotes } = parseReplyContexts(text);
52
52
  if (quotes.length === 0)
53
53
  return { quotes: [], text };
54
- const unique = [];
55
- for (const quote of quotes) {
56
- if (quote && !unique.includes(quote))
57
- unique.push(quote);
58
- }
54
+ const unique = [...new Set(quotes.filter(Boolean))];
59
55
  return { quotes: unique, text: stripReplyContexts(text) };
60
56
  }
61
57
  /**
@@ -96,12 +92,16 @@ export function mergeComposerQuotes(restored, current) {
96
92
  * returns `current` itself.
97
93
  */
98
94
  export function restoreComposerQuotes(current, texts, makeId) {
95
+ const currentByText = new Map();
96
+ for (const quote of current)
97
+ if (!currentByText.has(quote.text))
98
+ currentByText.set(quote.text, quote);
99
99
  const restored = [];
100
100
  for (const raw of texts) {
101
101
  const text = raw.trim();
102
102
  if (!text || restored.some((quote) => quote.text === text))
103
103
  continue;
104
- restored.push(current.find((quote) => quote.text === text) ?? { id: makeId(), text });
104
+ restored.push(currentByText.get(text) ?? { id: makeId(), text });
105
105
  }
106
106
  if (restored.length === 0)
107
107
  return current;
@@ -249,7 +249,8 @@ export function planQuoteRequests({ requests, appliedIds, }) {
249
249
  * itself when nothing matches, so a no-op ack does not re-render.
250
250
  */
251
251
  export function acknowledgeQuoteRequests(requests, ids) {
252
- const remaining = requests.filter((request) => !ids.includes(request.id));
252
+ const acked = new Set(ids);
253
+ const remaining = requests.filter((request) => !acked.has(request.id));
253
254
  return remaining.length === requests.length ? requests : remaining;
254
255
  }
255
256
  /**
@@ -76,14 +76,14 @@ export function deserializeDraft(raw, currentUserId) {
76
76
  if (!Array.isArray(candidate.files))
77
77
  return null;
78
78
  const legacy = liftLegacyQuoteNodes(candidate.doc);
79
- const quotes = [];
79
+ const quotes = new Set();
80
80
  const stored = Array.isArray(candidate.quotes) ? candidate.quotes : [];
81
81
  for (const quote of [...stored, ...legacy.quotes]) {
82
82
  if (typeof quote !== 'string')
83
83
  continue;
84
84
  const text = quote.trim();
85
- if (text && !quotes.includes(text))
86
- quotes.push(text);
85
+ if (text)
86
+ quotes.add(text);
87
87
  }
88
88
  // Only `{ id, text }` with both set crosses back; anything else in storage is dropped.
89
89
  const storedPastes = Array.isArray(candidate.pastes) ? candidate.pastes : [];
@@ -101,7 +101,7 @@ export function deserializeDraft(raw, currentUserId) {
101
101
  u: candidate.u,
102
102
  doc: legacy.doc,
103
103
  files: candidate.files.filter(isRemote),
104
- quotes,
104
+ quotes: [...quotes],
105
105
  pastes,
106
106
  uploads,
107
107
  };
@@ -19,12 +19,13 @@ export function PastedTextCopy({ text }) {
19
19
  // The toolbar `ButtonGroup`'s look: `size-7` on the `secondary` fill, as `CloseButton`.
20
20
  return (_jsx(CopyButton, { code: text, size: "md", className: "bg-secondary hover:bg-secondary/90 shrink-0", label: t.raw('texte21f935f11d7') }));
21
21
  }
22
+ const graphemeSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl ? new Intl.Segmenter() : null;
22
23
  /** Word and character counts for the header. Characters are what the user sees: graphemes, else code points. */
23
24
  export function pastedTextCounts(text) {
24
25
  const words = text.trim() ? text.trim().split(/\s+/).length : 0;
25
26
  let chars = 0;
26
- if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
27
- for (const _segment of new Intl.Segmenter().segment(text))
27
+ if (graphemeSegmenter) {
28
+ for (const _segment of graphemeSegmenter.segment(text))
28
29
  chars++;
29
30
  }
30
31
  else
@@ -18,9 +18,10 @@ export const useQueuedDraftStore = create((set) => ({
18
18
  }),
19
19
  remove: (sessionId, clientMessageIds) => set((s) => {
20
20
  const drafts = s.bySession[sessionId];
21
- if (!drafts?.some((d) => clientMessageIds.includes(d.clientMessageId)))
21
+ const ids = new Set(clientMessageIds);
22
+ if (!drafts?.some((d) => ids.has(d.clientMessageId)))
22
23
  return s;
23
- return withSession(s, sessionId, drafts.filter((d) => !clientMessageIds.includes(d.clientMessageId)));
24
+ return withSession(s, sessionId, drafts.filter((d) => !ids.has(d.clientMessageId)));
24
25
  }),
25
26
  prune: (sessionId, listedClientMessageIds) => set((s) => {
26
27
  const drafts = s.bySession[sessionId];
@@ -33,7 +33,7 @@ export declare function ReminderFacts({ reminder, now, className, }: {
33
33
  now: number;
34
34
  className?: string;
35
35
  }): import("react").JSX.Element;
36
- /** Pause or resume, remove, and the link into the Reminders tab. */
36
+ /** Retry a failed run, pause or resume, remove, and the link into the Reminders tab. */
37
37
  export declare function ReminderActions({ reminder, controls, manageHref, onNavigate, className, }: {
38
38
  reminder: SessionReminder;
39
39
  controls: ReminderControls;
@@ -43,11 +43,14 @@ export declare function ReminderActions({ reminder, controls, manageHref, onNavi
43
43
  }): import("react").JSX.Element;
44
44
  export interface ReminderControls {
45
45
  pending: boolean;
46
+ running: boolean;
47
+ /** Fire the reminder once now. */
48
+ run: (reminder: SessionReminder) => void;
46
49
  setEnabled: (reminder: SessionReminder, enabled: boolean) => void;
47
50
  requestRemove: (reminder: SessionReminder) => void;
48
51
  }
49
52
  /**
50
- * The session's reminders with pause, resume and remove. Remove asks first:
53
+ * The session's reminders with run, pause, resume and remove. Remove asks first:
51
54
  * render `confirm` once next to whatever calls `requestRemove`.
52
55
  */
53
56
  export declare function useReminderControls(projectId: string | null, sessionId: string | null | undefined): {
@@ -59,6 +62,7 @@ export declare function useReminderControls(projectId: string | null, sessionId:
59
62
  remove: import("@tanstack/react-query").UseMutationResult<{
60
63
  ok: boolean;
61
64
  }, Error, string, unknown>;
65
+ run: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, string, unknown>;
62
66
  data: {
63
67
  reminders: SessionReminder[];
64
68
  };
@@ -96,6 +100,7 @@ export declare function useReminderControls(projectId: string | null, sessionId:
96
100
  remove: import("@tanstack/react-query").UseMutationResult<{
97
101
  ok: boolean;
98
102
  }, Error, string, unknown>;
103
+ run: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, string, unknown>;
99
104
  data: {
100
105
  reminders: SessionReminder[];
101
106
  };
@@ -133,6 +138,7 @@ export declare function useReminderControls(projectId: string | null, sessionId:
133
138
  remove: import("@tanstack/react-query").UseMutationResult<{
134
139
  ok: boolean;
135
140
  }, Error, string, unknown>;
141
+ run: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, string, unknown>;
136
142
  data: undefined;
137
143
  error: Error;
138
144
  isError: true;
@@ -168,6 +174,7 @@ export declare function useReminderControls(projectId: string | null, sessionId:
168
174
  remove: import("@tanstack/react-query").UseMutationResult<{
169
175
  ok: boolean;
170
176
  }, Error, string, unknown>;
177
+ run: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, string, unknown>;
171
178
  data: undefined;
172
179
  error: null;
173
180
  isError: false;
@@ -203,6 +210,7 @@ export declare function useReminderControls(projectId: string | null, sessionId:
203
210
  remove: import("@tanstack/react-query").UseMutationResult<{
204
211
  ok: boolean;
205
212
  }, Error, string, unknown>;
213
+ run: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, string, unknown>;
206
214
  data: undefined;
207
215
  error: null;
208
216
  isError: false;
@@ -238,6 +246,7 @@ export declare function useReminderControls(projectId: string | null, sessionId:
238
246
  remove: import("@tanstack/react-query").UseMutationResult<{
239
247
  ok: boolean;
240
248
  }, Error, string, unknown>;
249
+ run: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, string, unknown>;
241
250
  data: {
242
251
  reminders: SessionReminder[];
243
252
  };