@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
@@ -1,13 +1,3 @@
1
- /**
2
- * The masthead of a printed conversation.
3
- *
4
- * A PDF of a session is read away from the app — attached to a ticket, mailed
5
- * to a customer, filed as a record — so it has to say what it is on its own
6
- * face. On screen this renders nothing (`print.css` keeps `[data-print-header]`
7
- * display:none until the print media query), and it is in the DOM at all times
8
- * so that printing needs no re-render: `window.print()` is synchronous, and a
9
- * React commit scheduled beside it is not guaranteed to land first.
10
- */
11
1
  export interface SessionPrintHeaderProps {
12
2
  title: string;
13
3
  projectName?: string | null;
@@ -1,15 +1,30 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * The masthead of a printed conversation.
5
+ *
6
+ * A PDF of a session is read away from the app — attached to a ticket, mailed
7
+ * to a customer, filed as a record — so it has to say what it is on its own
8
+ * face. On screen this renders nothing (`print.css` keeps `[data-print-header]`
9
+ * display:none until the print media query), and it is in the DOM at all times
10
+ * so that printing needs no re-render: `window.print()` is synchronous, and a
11
+ * React commit scheduled beside it is not guaranteed to land first.
12
+ *
13
+ * The agent is named the way the composer names it: a host's `agentLabels`
14
+ * entry wins over the raw agent name.
15
+ */
16
+ import { useUiHostConfig } from '../../providers/ui-provider.js';
17
+ const printedAtFormat = new Intl.DateTimeFormat('en-GB', {
18
+ day: 'numeric',
19
+ month: 'long',
20
+ year: 'numeric',
21
+ hour: '2-digit',
22
+ minute: '2-digit',
23
+ hour12: false,
24
+ });
3
25
  /** `16 September 2026 at 20:41` — unambiguous across locales, no slashes. */
4
26
  export function formatPrintedAt(date) {
5
- return new Intl.DateTimeFormat('en-GB', {
6
- day: 'numeric',
7
- month: 'long',
8
- year: 'numeric',
9
- hour: '2-digit',
10
- minute: '2-digit',
11
- hour12: false,
12
- }).format(date);
27
+ return printedAtFormat.format(date);
13
28
  }
14
29
  /** The meta line under the title: the parts that exist, separated by a middot. */
15
30
  export function printHeaderMeta(input) {
@@ -19,9 +34,10 @@ export function printHeaderMeta(input) {
19
34
  .join(' · ');
20
35
  }
21
36
  export function SessionPrintHeader({ title, projectName, agentName, now, }) {
37
+ const { agentLabels } = useUiHostConfig();
22
38
  const meta = printHeaderMeta({
23
39
  projectName,
24
- agentName,
40
+ agentName: (agentName && agentLabels?.[agentName]) || agentName,
25
41
  printedAt: formatPrintedAt(now ?? new Date()),
26
42
  });
27
43
  return (
@@ -113,7 +113,7 @@ export function resolveResumeOverlay(input) {
113
113
  * skeleton rows instead of a restart card or the boot screen.
114
114
  */
115
115
  export function hasOrExpectsTranscript(input) {
116
- return input.hasTranscript || input.savedTranscript === 'loading';
116
+ return input.hasTranscript || input.savedTranscript !== 'none';
117
117
  }
118
118
  /**
119
119
  * Should the local "brand new session" hint be forgotten?
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
+ import { parseSessionReferences } from './message-parsing.js';
2
3
  import type { SentAttachment } from '../composer/sent-attachment-previews.js';
3
4
  import { type AttachmentUploadStatus } from '../turn/user-message.js';
4
5
  /**
@@ -82,8 +83,11 @@ busy, deliveryStatus, className, }: {
82
83
  * app: chip in the composer, underline in the optimistic bubble, underline
83
84
  * again in the server turn.
84
85
  */
85
- export declare function HighlightMentions({ text, agentNames, onFileClick, }: {
86
+ export declare function HighlightMentions({ text, sessions: parsedSessions, agentNames, onFileClick, }: {
86
87
  text: string;
88
+ /** The session refs the caller already stripped from `text`. Then `text` is
89
+ * not parsed again: a second parse finds no refs and loses every title. */
90
+ sessions?: ReturnType<typeof parseSessionReferences>['sessions'];
87
91
  agentNames?: string[];
88
92
  onFileClick?: (path: string) => void;
89
93
  }): import("react").JSX.Element;
@@ -85,7 +85,7 @@ function OptimisticUserBubble({ text, agentNames, onFileClick, deferPreview, sta
85
85
  // Strip every ref block the composer folded into the prompt, in the order it
86
86
  // folded them in, so the bubble shows the sentence the user typed and the
87
87
  // attachments as tiles — never raw XML.
88
- const { quotes, files, cleanText, pastes } = useMemo(() => {
88
+ const { quotes, files, cleanText, pastes, sessions } = useMemo(() => {
89
89
  // Pastes first, as on the sent turn: a ref inside a paste is paste text.
90
90
  const { text: afterPastes, pastes } = splitPastedContent(text);
91
91
  const { cleanText: afterReply, quotes } = parseReplyContexts(afterPastes);
@@ -93,8 +93,8 @@ function OptimisticUserBubble({ text, agentNames, onFileClick, deferPreview, sta
93
93
  const { cleanText: afterProjects } = parseProjectReferences(afterFiles);
94
94
  const { cleanText: afterFileMentions } = parseFileMentionReferences(afterProjects);
95
95
  const { cleanText: afterAgentMentions } = parseAgentMentionReferences(afterFileMentions);
96
- const { cleanText } = parseSessionReferences(afterAgentMentions);
97
- return { quotes, files, cleanText, pastes };
96
+ const { cleanText, sessions } = parseSessionReferences(afterAgentMentions);
97
+ return { quotes, files, cleanText, pastes, sessions };
98
98
  }, [text]);
99
99
  // Quotes are drawn where they were written, by the same component the sent
100
100
  // message uses — see `QuotedMessageBody`.
@@ -129,7 +129,7 @@ function OptimisticUserBubble({ text, agentNames, onFileClick, deferPreview, sta
129
129
  }));
130
130
  return [...pastes.map(pastedAttachment), ...fromText, ...fromStaged];
131
131
  }, [pastes, files, deferPreview, staged]);
132
- return (_jsxs("div", { className: "ml-auto flex w-full max-w-[80%] flex-col items-end gap-2 self-end", children: [attachments.length > 0 && (_jsx(MessageAttachments, { attachments: attachments, status: uploadStatus })), (cleanText || quotedPieces) && (_jsx("div", { className: cn(BUBBLE_SURFACE, 'w-fit overflow-hidden'), children: quotedPieces ? (_jsx(QuotedMessageBody, { pieces: quotedPieces, renderText: (runText) => (_jsx(HighlightMentions, { text: runText, agentNames: agentNames, onFileClick: onFileClick })) })) : (_jsx("p", { className: BUBBLE_TEXT, children: _jsx(HighlightMentions, { text: cleanText, agentNames: agentNames, onFileClick: onFileClick }) })) })), _jsx(UserMessageActions, { timestamp: null, copyText: expandPastedContent(text), deliveryStatus: deliveryStatus })] }));
132
+ return (_jsxs("div", { className: "ml-auto flex w-full max-w-[80%] flex-col items-end gap-2 self-end", children: [attachments.length > 0 && (_jsx(MessageAttachments, { attachments: attachments, status: uploadStatus })), (cleanText || quotedPieces) && (_jsx("div", { className: cn(BUBBLE_SURFACE, 'w-fit overflow-hidden'), children: quotedPieces ? (_jsx(QuotedMessageBody, { pieces: quotedPieces, renderText: (runText) => (_jsx(HighlightMentions, { text: runText, sessions: sessions, agentNames: agentNames, onFileClick: onFileClick })) })) : (_jsx("p", { className: BUBBLE_TEXT, children: _jsx(HighlightMentions, { text: cleanText, sessions: sessions, agentNames: agentNames, onFileClick: onFileClick }) })) })), _jsx(UserMessageActions, { timestamp: null, copyText: expandPastedContent(text), deliveryStatus: deliveryStatus })] }));
133
133
  }
134
134
  /**
135
135
  * Highlight @mentions in plain text (for optimistic & user messages).
@@ -141,11 +141,13 @@ function OptimisticUserBubble({ text, agentNames, onFileClick, deferPreview, sta
141
141
  * app: chip in the composer, underline in the optimistic bubble, underline
142
142
  * again in the server turn.
143
143
  */
144
- export function HighlightMentions({ text, agentNames, onFileClick, }) {
144
+ export function HighlightMentions({ text, sessions: parsedSessions, agentNames, onFileClick, }) {
145
145
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
146
146
  // Strip every ref block (project/file/agent/session) before processing
147
147
  // inline @ mentions so the visible text never shows raw XML.
148
148
  const { cleanText, sessions } = useMemo(() => {
149
+ if (parsedSessions)
150
+ return { cleanText: text, sessions: parsedSessions };
149
151
  const a = parseProjectReferences(text);
150
152
  const b = parseFileMentionReferences(a.cleanText);
151
153
  const c = parseAgentMentionReferences(b.cleanText);
@@ -154,7 +156,7 @@ export function HighlightMentions({ text, agentNames, onFileClick, }) {
154
156
  cleanText: d.cleanText,
155
157
  sessions: d.sessions,
156
158
  };
157
- }, [text]);
159
+ }, [text, parsedSessions]);
158
160
  const sessionTitles = useMemo(() => sessions.map((s) => s.title), [sessions]);
159
161
  const segments = useMemo(() => {
160
162
  const built = buildMentionSegments({ text: cleanText, sessionTitles, agentNames });
@@ -2,6 +2,11 @@ import { type UseSessionResult } from '@kortix/sdk/react';
2
2
  export { SessionReportCard, deriveTurnErrorAbortState, deriveTurnErrorPresentation } from './transcript.js';
3
3
  interface SessionChatProps {
4
4
  sessionId: string;
5
+ /** A host's reply quote (`SessionThreadProps.quote`): each new id adds its text to the composer's quotes. */
6
+ hostQuote?: {
7
+ id: number;
8
+ text: string;
9
+ } | null;
5
10
  /** Durable Kortix project session id used by project-session APIs. */
6
11
  projectSessionId?: string;
7
12
  /** Complete SDK state for the root session. Omit for a read-only child session. */
@@ -59,4 +64,4 @@ interface SessionChatProps {
59
64
  */
60
65
  deferComposerFocus?: boolean;
61
66
  }
62
- export declare function SessionChat({ sessionId, projectSessionId, sessionState, projectId, boundAgentName, headerLeadingAction, hideHeader, readOnly, inputReplacement, initialScrollTop, savedHistoryScope, onContentReady, deferComposerFocus, }: SessionChatProps): import("react").JSX.Element;
67
+ export declare function SessionChat({ sessionId, projectSessionId, sessionState, projectId, boundAgentName, headerLeadingAction, hideHeader, readOnly, inputReplacement, initialScrollTop, savedHistoryScope, onContentReady, deferComposerFocus, hostQuote, }: SessionChatProps): import("react").JSX.Element;
@@ -171,7 +171,7 @@ const TRANSCRIPT_THROTTLE_MS = 50;
171
171
  /** `useSessionMessages` input when no `useSession` owns this chat: reads nothing. */
172
172
  const DETACHED_SESSION_MESSAGES = { projectId: '', sessionId: '', runtimeSessionId: null };
173
173
  const NO_FULL_TEXTS = new Map();
174
- export function SessionChat({ sessionId, projectSessionId, sessionState, projectId, boundAgentName, headerLeadingAction, hideHeader, readOnly, inputReplacement, initialScrollTop, savedHistoryScope, onContentReady, deferComposerFocus, }) {
174
+ export function SessionChat({ sessionId, projectSessionId, sessionState, projectId, boundAgentName, headerLeadingAction, hideHeader, readOnly, inputReplacement, initialScrollTop, savedHistoryScope, onContentReady, deferComposerFocus, hostQuote, }) {
175
175
  const tHardcodedUi = useTranslations('hardcodedUi');
176
176
  const tQueue = useTranslations('threads');
177
177
  const tComposerAttachments = useTranslations('hardcodedUi.composerAttachments');
@@ -283,6 +283,17 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
283
283
  }
284
284
  lastScrollTopRef.current = top;
285
285
  }, []);
286
+ // A host's quote: each new id is one more request, like a "Reply" click.
287
+ const hostQuoteIdRef = useRef(null);
288
+ useEffect(() => {
289
+ const text = hostQuote?.text.trim();
290
+ if (!hostQuote || !text || hostQuoteIdRef.current === hostQuote.id)
291
+ return;
292
+ hostQuoteIdRef.current = hostQuote.id;
293
+ quoteRequestIdRef.current += 1;
294
+ const request = { id: quoteRequestIdRef.current, text };
295
+ setQuoteRequests((current) => [...current, request]);
296
+ }, [hostQuote]);
286
297
  // When user clicks "Reply" in the popup
287
298
  const handleSelectionReply = useCallback(() => {
288
299
  if (!selectionPopup)
@@ -378,7 +389,9 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
378
389
  const messages = thread?.messages.length ? thread.messages : undefined;
379
390
  // The thread for handlers that read it at call time (the optimistic answers cache).
380
391
  const threadRef = useRef(thread);
381
- threadRef.current = thread;
392
+ useLayoutEffect(() => {
393
+ threadRef.current = thread;
394
+ }, [thread]);
382
395
  const messagesLoading = syncMessagesLoading;
383
396
  // Who wrote each user message. A one-person session stays unlabelled.
384
397
  const viewerId = useUiViewerId();
@@ -1274,6 +1287,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
1274
1287
  autoLoadedPages,
1275
1288
  handleLoadOlder,
1276
1289
  scrollRef,
1290
+ spacerElRef,
1277
1291
  sessionId,
1278
1292
  transcriptOnLog,
1279
1293
  ]);
@@ -1477,6 +1491,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
1477
1491
  held: heldTurnIds,
1478
1492
  pricing: pricingLookup,
1479
1493
  hideOutOfContext,
1494
+ foldCompactions: true,
1480
1495
  }), [lastTurnWorking, heldTurnIds, pricingLookup, hideOutOfContext]);
1481
1496
  // WHICH turn carries the shimmer (`live`), and which user bubbles are still
1482
1497
  // queued at the agent (`queued`): the working projection's turn, else this
@@ -1494,7 +1509,9 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
1494
1509
  const harnessSwitches = useMemo(() => sessionLogHarnessSwitches(sessionRecord?.harness?.history, turns), [sessionRecord?.harness?.history, turns]);
1495
1510
  // P5.6: load the whole of every text a turn shows as a preview, on request only.
1496
1511
  const fullTextsRef = useRef(loaded);
1497
- fullTextsRef.current = loaded;
1512
+ useLayoutEffect(() => {
1513
+ fullTextsRef.current = loaded;
1514
+ });
1498
1515
  const loadFullTexts = useCallback((turnId, refs) => {
1499
1516
  const key = fullTextsRef.current.key;
1500
1517
  const mark = (state) => setFullTexts((prev) => {
@@ -1948,7 +1965,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
1948
1965
  // A compaction turn of any state (`turn.compaction`), so a failed attempt's
1949
1966
  // turn suppresses the optimistic "Compacting…" marker instead of leaving it
1950
1967
  // shimmering beside the failure row.
1951
- const hasCompactionTurn = useMemo(() => turns.some((turn) => turn.compaction !== null), [turns]);
1968
+ const hasCompactionTurn = useMemo(() => turns.some((turn) => turn.compaction !== null || turn.blocks.some((block) => block.type === 'compaction')), [turns]);
1952
1969
  // Index of the LAST compaction turn (any state). Every FAILED attempt
1953
1970
  // before it is history the reader retried past — a run of retries renders
1954
1971
  // as ONE failure row (the latest), not a stack of near-identical lines.
@@ -3189,6 +3206,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
3189
3206
  ? 'duration-slow max-h-130 translate-y-0 opacity-100'
3190
3207
  : 'duration-slow pointer-events-none max-h-0 -translate-y-1 opacity-0'), children: _jsx(QuestionPrompt, { ref: questionPromptRef, request: renderedQuestion, onReply: handleQuestionReply, onReject: handleQuestionReject, onActionChange: handleQuestionActionChange }, renderedQuestion.id) })) : null] })), [
3191
3208
  sessionId,
3209
+ sessionScopeAgentName,
3192
3210
  pendingPermissions,
3193
3211
  handlePermissionReply,
3194
3212
  renderedQuestion,
@@ -3196,10 +3214,6 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
3196
3214
  handleQuestionReply,
3197
3215
  handleQuestionReject,
3198
3216
  handleQuestionActionChange,
3199
- tHardcodedUi,
3200
- local.model.fallback,
3201
- local.model.list,
3202
- handleModelChange,
3203
3217
  ]);
3204
3218
  // The rewound-path notice lives on the composer toolbar, beside send/stop —
3205
3219
  // send is what commits the path, so the control sits at the moment of
@@ -3382,12 +3396,20 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
3382
3396
  // when the host router observes history and routes through it otherwise.
3383
3397
  shallowNavigate(qs ? `${pathname}?${qs}` : pathname, { replace: true });
3384
3398
  }, [readOnly, pathname, searchParams, shallowNavigate]);
3399
+ // A session tab stays mounted, so a later `?cr=` (the turn-done toast's
3400
+ // Review) must open the dialog too, not only the one read at mount.
3401
+ const crParam = readOnly ? null : (searchParams?.get('cr') ?? null);
3402
+ useEffect(() => {
3403
+ if (crParam)
3404
+ setOpenCrIdState(crParam);
3405
+ }, [crParam]);
3385
3406
  const handleOpenOutcome = useCallback((outcome) => {
3386
3407
  // Change requests are the only kind a turn produces. `external` exists on
3387
3408
  // the union for `setup-links/setup-link-button.tsx`, which renders this
3388
3409
  // card inline in prose and handles its own open.
3389
- if (outcome.kind === 'change_request')
3390
- setOpenCrId(outcome.id.slice('cr:'.length));
3410
+ if (outcome.kind === 'change_request') {
3411
+ setOpenCrId(outcome.action.target ?? outcome.id.slice('cr:'.length));
3412
+ }
3391
3413
  }, [setOpenCrId]);
3392
3414
  // While the session is still connecting / loading its content, render ONLY the
3393
3415
  // staged loader — never the session shell (header + input) at the same time.
@@ -3420,7 +3442,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
3420
3442
  : welcomeWallpaper, !hideHeader && (_jsx(SessionSiteHeader, { sessionId: sessionId, sessionTitle: session?.title || 'Untitled', leadingAction: headerLeadingAction, parent: parentCrumb })), _jsx(SessionContextModal, { open: contextModalOpen, onOpenChange: setContextModalOpen,
3421
3443
  // A chat with no `useSession` source: the raw explorer lists the
3422
3444
  // rows `useSessionSync` read for it.
3423
- detachedRows: sessionState ? undefined : hookMessages, source: sessionState ?? DETACHED_SESSION_MESSAGES, session: session, providers: providers, allSessions: allSessions, servedModel: sessionServedModel, billedCost: sessionBilledCost(modelUsage) }), _jsx(CompactModal, { sessionId: sessionId, open: compactModalOpen, onOpenChange: setCompactModalOpen }), _jsx(ChangeRequestDetailDialog, { crId: openCrId, onClose: () => setOpenCrId(null) }), _jsxs(SessionBodyRow, { actionPanel: !hideHeader && !readOnly, children: [isNotFound ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 text-center", children: [_jsx("div", { className: "text-muted-foreground text-sm", children: tHardcodedUi.raw('componentsSessionSessionChat.line5821JsxTextThisSessionIsNotAccessibleRightNow') }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { href: "/", prefetch: true, onClick: () => {
3445
+ detachedRows: sessionState ? undefined : hookMessages, source: sessionState ?? DETACHED_SESSION_MESSAGES, session: session, providers: providers, allSessions: allSessions, servedModel: sessionServedModel, billedCost: sessionBilledCost(modelUsage) }), _jsx(CompactModal, { sessionId: sessionId, open: compactModalOpen, onOpenChange: setCompactModalOpen }), _jsx(ChangeRequestDetailDialog, { crId: openCrId, onClose: () => setOpenCrId(null), onOpenChangeRequest: setOpenCrId }), _jsxs(SessionBodyRow, { actionPanel: !hideHeader && !readOnly, children: [isNotFound ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 text-center", children: [_jsx("div", { className: "text-muted-foreground text-sm", children: tHardcodedUi.raw('componentsSessionSessionChat.line5821JsxTextThisSessionIsNotAccessibleRightNow') }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { href: "/", prefetch: true, onClick: () => {
3424
3446
  try {
3425
3447
  if (sessionId)
3426
3448
  useTabStore.getState().closeTab?.(sessionId);
@@ -3448,7 +3470,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
3448
3470
  // it — the "extra inset at the bottom of the session".
3449
3471
  className: SESSION_TRANSCRIPT_CLASS, children: [_jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx(SessionPrintHeader, { title: session?.title || 'Untitled', agentName: composerAgentName }), (hasOlder || showOlderLoading) && (_jsxs("div", { className: "mb-6 flex flex-col items-center gap-2", children: [hasOlder && (_jsx("div", { ref: olderSentinelRef, "aria-hidden": true, className: "h-px w-full" })), showOlderLoading && (_jsxs("div", { role: "status", className: "text-muted-foreground flex items-center gap-2 py-1 text-xs", children: [_jsx(Loading, { className: "size-3.5 shrink-0" }), tHardcodedUi.raw('i18nComplete.text85bf890776a7')] })), !showOlderLoading &&
3450
3472
  !olderPullFailed &&
3451
- olderAutoloadExhausted({ hasOlder, autoLoadedPages }) && (_jsx(Button, { type: "button", variant: "outline-ghost", size: "sm", onClick: () => void handleLoadOlder(), children: tHardcodedUi.raw('i18nComplete.textf17671d83db0') })), olderPullFailed && !showOlderLoading && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: tHardcodedUi.raw('i18nComplete.textb03a0041ce33') }), _jsx(Button, { type: "button", variant: "outline-ghost", size: "sm", onClick: () => void handleLoadOlder(), children: tHardcodedUi.raw('i18nComplete.text942087cc2d41') })] }))] })), restoreNotice && !restoreDismissed && (_jsx(RestoreNotice, { notice: restoreNotice, onDismiss: () => dismissRestore(historyKey, restoreNotice.generation) })), _jsx(SessionOutcomesProvider, { projectSessionId: projectSessionId, turnSpans: turnSpans, onOpen: handleOpenOutcome, children: _jsxs(ToolActivateContext.Provider, { value: toolActivate, children: [showFirstPromptPreview &&
3473
+ olderAutoloadExhausted({ hasOlder, autoLoadedPages }) && (_jsx(Button, { type: "button", variant: "outline-ghost", size: "sm", onClick: () => void handleLoadOlder(), children: tHardcodedUi.raw('i18nComplete.textf17671d83db0') })), olderPullFailed && !showOlderLoading && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: tHardcodedUi.raw('i18nComplete.textb03a0041ce33') }), _jsx(Button, { type: "button", variant: "outline-ghost", size: "sm", onClick: () => void handleLoadOlder(), children: tHardcodedUi.raw('i18nComplete.text942087cc2d41') })] }))] })), restoreNotice && !restoreDismissed && (_jsx(RestoreNotice, { notice: restoreNotice, onDismiss: () => dismissRestore(historyKey, restoreNotice.generation) })), _jsx(SessionOutcomesProvider, { projectSessionId: projectSessionId, turnSpans: turnSpans, busy: effectiveBusy, onOpen: handleOpenOutcome, children: _jsxs(ToolActivateContext.Provider, { value: toolActivate, children: [showFirstPromptPreview &&
3452
3474
  firstPromptSource &&
3453
3475
  queuedSyntheticMessages.length === 0 && (_jsx(OptimisticTurn, { text: buildOptimisticPromptTextWithUploads(firstPromptSource.text, firstPromptSource.files),
3454
3476
  // Preserve a real failed-send status through the
@@ -15,6 +15,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
15
15
  */
16
16
  import { ArrowsLeftRightIcon, ClockCounterClockwiseIcon, XIcon } from '@phosphor-icons/react';
17
17
  import { sessionLogFullTextAction, sessionLogReferencedTexts, } from '@kortix/sdk';
18
+ import { useMemo } from 'react';
18
19
  import { Button } from '../../primitives/button.js';
19
20
  import { InfoBanner } from '../../primitives/info-banner.js';
20
21
  import { useLocale, useTranslations } from '../../i18n/index.js';
@@ -51,7 +52,8 @@ function missingLabel(t, kind, count) {
51
52
  export function RestoreNotice({ notice, onDismiss }) {
52
53
  const t = useTranslations('sessionUi.history');
53
54
  const locale = useLocale();
54
- const items = new Intl.ListFormat(locale, { style: 'narrow', type: 'unit' }).format(notice.missing.map((m) => missingLabel(t, m.kind, m.count)));
55
+ const listFormat = useMemo(() => new Intl.ListFormat(locale, { style: 'narrow', type: 'unit' }), [locale]);
56
+ const items = listFormat.format(notice.missing.map((m) => missingLabel(t, m.kind, m.count)));
55
57
  return (
56
58
  // `info`: the neutral tone keeps `Alert`'s default fill under a hairline (two boundaries).
57
59
  _jsxs(InfoBanner, { tone: "info", role: "status", "data-restore-notice": "", className: "mb-6 rounded-md", icon: ClockCounterClockwiseIcon, title: t('restoreTitle'), action: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onDismiss, "aria-label": t('restoreDismiss'), className: "text-muted-foreground", children: _jsx(XIcon, { className: "size-4" }) }), children: [notice.missing.length > 0 && _jsxs("span", { children: [t('restoreMissing', { items }), " "] }), t('restoreAgent')] }));
@@ -21,6 +21,15 @@ export interface SessionThreadProps {
21
21
  hideHeader?: boolean;
22
22
  /** Hide the composer: a transcript to read, not to answer. */
23
23
  readOnly?: boolean;
24
+ /**
25
+ * A quote the host adds to the composer's reply quotes (the card above the
26
+ * input), as a transcript selection's "Reply" does. Each new `id` adds its
27
+ * `text` once.
28
+ */
29
+ quote?: {
30
+ id: number;
31
+ text: string;
32
+ } | null;
24
33
  className?: string;
25
34
  }
26
- export declare function SessionThread({ session, hideHeader, readOnly, className }: SessionThreadProps): import("react").JSX.Element;
35
+ export declare function SessionThread({ session, hideHeader, readOnly, quote, className }: SessionThreadProps): import("react").JSX.Element;
@@ -7,7 +7,7 @@ import { SessionPanelFallback } from '../panel/panel-fallback.js';
7
7
  import { SessionChat } from './session-chat.js';
8
8
  import { SessionStartingLoader } from '../status/session-starting-loader.js';
9
9
  import '../tool/tools/register.js';
10
- export function SessionThread({ session, hideHeader = false, readOnly = false, className }) {
10
+ export function SessionThread({ session, hideHeader = false, readOnly = false, quote, className }) {
11
11
  const { projectId, sessionId, runtimeSessionId } = session;
12
- return (_jsx(ProjectScope, { projectId: projectId, sessionId: sessionId, children: _jsx(ProviderPoolDraftBoundary, { identity: `${projectId}/${sessionId}`, children: _jsxs("div", { "data-slot": "session-thread", className: cn('relative flex h-full min-h-0 flex-col', className), children: [runtimeSessionId ? (_jsx(SessionChat, { sessionId: runtimeSessionId, projectSessionId: sessionId, projectId: projectId, sessionState: session, hideHeader: hideHeader, readOnly: readOnly }, runtimeSessionId)) : (_jsx(SessionStartingLoader, { stage: session.stage === 'starting' ? 'starting' : 'provisioning', projectId: projectId, sessionId: sessionId })), _jsx(SessionPanelFallback, { projectId: projectId, projectSessionId: sessionId, runtimeSessionId: runtimeSessionId })] }) }) }));
12
+ return (_jsx(ProjectScope, { projectId: projectId, sessionId: sessionId, children: _jsx(ProviderPoolDraftBoundary, { identity: `${projectId}/${sessionId}`, children: _jsxs("div", { "data-slot": "session-thread", className: cn('relative flex h-full min-h-0 flex-col', className), children: [runtimeSessionId ? (_jsx(SessionChat, { sessionId: runtimeSessionId, projectSessionId: sessionId, projectId: projectId, sessionState: session, hideHeader: hideHeader, readOnly: readOnly, hostQuote: quote }, runtimeSessionId)) : (_jsx(SessionStartingLoader, { stage: session.stage === 'starting' ? 'starting' : 'provisioning', projectId: projectId, sessionId: sessionId })), _jsx(SessionPanelFallback, { projectId: projectId, projectSessionId: sessionId, runtimeSessionId: runtimeSessionId })] }) }) }));
13
13
  }
@@ -43,6 +43,8 @@ import { Disclosure, DisclosureContent, DisclosureTrigger } from '../../primitiv
43
43
  import { useUserPreferencesStore } from '../../internal/stores/user-preferences-store.js';
44
44
  import { SubSessionModal } from './sub-session-modal.js';
45
45
  import { ToolPartRenderer, TurnLiveContext } from '../tool/tool-renderers.js';
46
+ import { RecoveredCallsContext } from '../tool/shared/infrastructure.js';
47
+ import { recoveredShellFailures } from '../turn/recovered-failures.js';
46
48
  import { cn } from '../../lib/utils.js';
47
49
  import { extractKortixSystemMessages, extractSessionReport, stripKortixSystemTags, } from '../../internal/lib/utils/kortix-system-tags.js';
48
50
  // Shared UI primitives (framework-agnostic, reusable on mobile)
@@ -959,6 +961,8 @@ function TurnUserBubble(props) {
959
961
  ? () => onRemoveQueued(pendingPrompt.prompt_id)
960
962
  : undefined })) : queuedStatus === 'sending' || queuedStatus === 'queued' || queuedStatus === 'interrupted' ? (_jsx(QueuedPromptProgress, { state: queuedStatus })) : undefined }) })) }));
961
963
  }
964
+ /** One shared empty set: a live turn has no recovered calls yet. */
965
+ const NO_RECOVERED_CALLS = new Set();
962
966
  /** The assistant's part tree: bursts, standalone deliverables, and prose
963
967
  * between them, under one live-context provider. */
964
968
  function TurnToolBlocks(props) {
@@ -972,6 +976,8 @@ function TurnToolBlocks(props) {
972
976
  // Settled turns only: while working the split is null anyway, and the
973
977
  // filter re-runs the burst steps on every SSE frame.
974
978
  const split = useMemo(() => (fold && !working ? splitSessionLogTurnWork(segments.filter(segmentRendersSomething), { working }) : null), [fold, segments, working]);
979
+ // Settled turns only: while the turn runs, the next step has not happened yet.
980
+ const recovered = useMemo(() => (working ? NO_RECOVERED_CALLS : recoveredShellFailures(turnBlocks(turn))), [turn, working]);
975
981
  const renderSegment = (segment) => {
976
982
  const key = turnSegmentKey(segment);
977
983
  if (key === null)
@@ -984,13 +990,13 @@ function TurnToolBlocks(props) {
984
990
  // will. Provided here rather than per-row because `working` is a fact
985
991
  // about the TURN, and this block is the turn's whole part tree.
986
992
  // See `TurnLiveContext`.
987
- _jsx(TurnLiveContext.Provider, { value: working, children: _jsx("div", { className: "space-y-3", children: split
988
- ? [
989
- _jsx(TurnWorkAccordion, { durationMs: durationMs, children: split.work.map(renderSegment) }, "turn-work"),
990
- ...split.hoisted.map(renderSegment),
991
- ...split.final.map(renderSegment),
992
- ]
993
- : segments.map(renderSegment) }) })) }));
993
+ _jsx(TurnLiveContext.Provider, { value: working, children: _jsx(RecoveredCallsContext.Provider, { value: recovered, children: _jsx("div", { className: "space-y-3", children: split
994
+ ? [
995
+ _jsx(TurnWorkAccordion, { durationMs: durationMs, children: split.work.map(renderSegment) }, "turn-work"),
996
+ ...split.hoisted.map(renderSegment),
997
+ ...split.final.map(renderSegment),
998
+ ]
999
+ : segments.map(renderSegment) }) }) })) }));
994
1000
  }
995
1001
  /** One segment of the turn's steps section: a burst, a show group, a
996
1002
  * standalone deliverable, or prose between bursts. Keyed by the first
@@ -59,6 +59,8 @@ export function parseListing(output, dir) {
59
59
  return files.length ? files : null;
60
60
  }
61
61
  export function ToolListRow({ icon, name, dir, trailing, chevron, onClick, onNameClick, disabled = false, title, }) {
62
+ const rowAction = onClick && !disabled ? onClick : undefined;
63
+ const nameAction = onNameClick && !disabled ? onNameClick : undefined;
62
64
  return (_jsxs("div", { className: cn(
63
65
  // Same row grammar as WebSourceRow: leading glyph, the name taking the
64
66
  // free space, and the secondary detail parked on the right. A file list
@@ -68,10 +70,25 @@ export function ToolListRow({ icon, name, dir, trailing, chevron, onClick, onNam
68
70
  ? 'cursor-default opacity-70'
69
71
  : onClick
70
72
  ? 'hover:bg-muted cursor-pointer'
71
- : undefined), onClick: onClick && !disabled ? onClick : undefined, title: title, children: [chevron && (_jsx(ChevronRight, { className: cn('text-muted-foreground/60 size-3.5 shrink-0 transition-transform', chevron === 'expanded' && 'rotate-90') })), _jsx("span", { className: "text-muted-foreground/60 group-hover:text-foreground/70 shrink-0 transition-colors [&>svg]:size-4", children: icon }), _jsx("span", { className: cn('text-foreground min-w-0 flex-1 truncate font-mono text-sm', onNameClick && !disabled && 'hover:text-primary cursor-pointer transition-colors'), onClick: onNameClick && !disabled
73
+ : undefined), role: rowAction ? 'button' : undefined, tabIndex: rowAction ? 0 : undefined, onClick: rowAction, onKeyDown: rowAction
74
+ ? (e) => {
75
+ if (e.target !== e.currentTarget || (e.key !== 'Enter' && e.key !== ' '))
76
+ return;
77
+ e.preventDefault();
78
+ rowAction();
79
+ }
80
+ : undefined, title: title, children: [chevron && (_jsx(ChevronRight, { className: cn('text-muted-foreground/60 size-3.5 shrink-0 transition-transform', chevron === 'expanded' && 'rotate-90') })), _jsx("span", { className: "text-muted-foreground/60 group-hover:text-foreground/70 shrink-0 transition-colors [&>svg]:size-4", children: icon }), _jsx("span", { className: cn('text-foreground min-w-0 flex-1 truncate font-mono text-sm', onNameClick && !disabled && 'hover:text-primary cursor-pointer transition-colors'), role: nameAction ? 'button' : undefined, tabIndex: nameAction ? 0 : undefined, onClick: nameAction
72
81
  ? (e) => {
73
82
  e.stopPropagation();
74
- onNameClick();
83
+ nameAction();
84
+ }
85
+ : undefined, onKeyDown: nameAction
86
+ ? (e) => {
87
+ if (e.key !== 'Enter' && e.key !== ' ')
88
+ return;
89
+ e.preventDefault();
90
+ e.stopPropagation();
91
+ nameAction();
75
92
  }
76
93
  : undefined, children: name }), dir && (_jsx("span", { className: "text-muted-foreground max-w-[40%] shrink-0 truncate font-mono text-sm", children: dir })), trailing !== undefined && trailing !== null && (_jsx("span", { className: "text-muted-foreground shrink-0 text-sm tabular-nums", children: trailing }))] }));
77
94
  }
@@ -59,6 +59,13 @@ export declare const StalePendingContext: import("react").Context<boolean>;
59
59
  * reading without opting in.
60
60
  */
61
61
  export declare const TurnLiveContext: import("react").Context<boolean>;
62
+ /**
63
+ * The ids of the failed commands in a settled turn that the agent moved past: a
64
+ * later step of the same turn worked (`recoveredShellFailures`). Their rows keep
65
+ * "Command failed" and the exit code, in the muted ink; red stays for a failure
66
+ * the turn ended on (KRTX-3021). Empty outside a settled turn.
67
+ */
68
+ export declare const RecoveredCallsContext: import("react").Context<ReadonlySet<string>>;
62
69
  export declare const ToolDurationContext: import("react").Context<number | undefined>;
63
70
  export declare function shouldShowToolPartInActionsPanel(call: ToolCallBlock): boolean;
64
71
  export declare const ToolActivateContext: import("react").Context<((callID: string) => void) | null>;
@@ -63,6 +63,13 @@ export const StalePendingContext = createContext(false);
63
63
  * reading without opting in.
64
64
  */
65
65
  export const TurnLiveContext = createContext(false);
66
+ /**
67
+ * The ids of the failed commands in a settled turn that the agent moved past: a
68
+ * later step of the same turn worked (`recoveredShellFailures`). Their rows keep
69
+ * "Command failed" and the exit code, in the muted ink; red stays for a failure
70
+ * the turn ended on (KRTX-3021). Empty outside a settled turn.
71
+ */
72
+ export const RecoveredCallsContext = createContext(new Set());
66
73
  export const ToolDurationContext = createContext(undefined);
67
74
  // Background memory plumbing (searches/gets and raw memory/ reads) stays
68
75
  // out of the Actions panel. The memory editor tool itself ('memory'/'oc-memory')
@@ -15,9 +15,13 @@ import { useMemo } from 'react';
15
15
  import { MD_FLUSH_CLASSES } from './infrastructure-preview.js';
16
16
  import { cleanErrorMessage, formatJsonFailureOutput, looksLikeError, parseJsonFailure } from '@kortix/sdk';
17
17
  export function JsonFailureOutputCard({ failure, }) {
18
- const summary = cleanErrorMessage(failure.errorSummary);
19
- const detail = failure.nestedMessage ? cleanErrorMessage(failure.nestedMessage) : undefined;
20
- return (_jsxs("div", { className: "flex items-start gap-2.5 px-3 py-2.5 text-xs", children: [_jsx("span", { className: cn('mt-px flex size-5 shrink-0 items-center justify-center rounded-sm', STATUS_BG.destructive), children: _jsx(CircleAlert, { className: cn('size-3.5', STATUS_TEXT.destructive) }) }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [_jsx("p", { className: "text-foreground/90 text-xs leading-relaxed text-pretty wrap-break-word", children: summary }), detail && detail !== summary && (_jsx("p", { className: "text-muted-foreground text-xs leading-relaxed text-pretty wrap-break-word", children: detail })), failure.hint && (_jsx("p", { className: "text-muted-foreground/80 text-xs leading-relaxed text-pretty wrap-break-word", children: failure.hint.trim() }))] }), typeof failure.status === 'number' && (_jsx("span", { className: "text-muted-foreground/60 shrink-0 font-mono text-xs tabular-nums", children: failure.status }))] }));
18
+ // A provider's sentence nested in the error body is the readable line; the
19
+ // raw `432 Error: {…}` around it is noise, and its status is in the chip
20
+ // (KRTX-3022).
21
+ const summary = failure.nestedMessage
22
+ ? cleanErrorMessage(failure.nestedMessage)
23
+ : cleanErrorMessage(failure.errorSummary);
24
+ return (_jsxs("div", { className: "flex items-start gap-2.5 px-3 py-2.5 text-xs", children: [_jsx("span", { className: cn('mt-px flex size-5 shrink-0 items-center justify-center rounded-sm', STATUS_BG.destructive), children: _jsx(CircleAlert, { className: cn('size-3.5', STATUS_TEXT.destructive) }) }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [_jsx("p", { className: "text-foreground text-xs leading-relaxed text-pretty wrap-break-word", children: summary }), failure.hint && (_jsx("p", { className: "text-muted-foreground text-xs leading-relaxed text-pretty wrap-break-word", children: failure.hint.trim() }))] }), typeof failure.status === 'number' && (_jsx("span", { className: "text-muted-foreground shrink-0 font-mono text-xs tabular-nums", children: failure.status }))] }));
21
25
  }
22
26
  export function ToolOutputFallback({ output, isStreaming = false, toolName, }) {
23
27
  const parsedJsonFailure = !isStreaming ? parseJsonFailure(output) : null;
@@ -65,7 +69,7 @@ function ToolOutputCard({ copyText, children }) {
65
69
  // have). On the panel the card IS the disclosure body, which already
66
70
  // supplies `px-3 py-3` — a top margin there is double-spacing, 18px
67
71
  // over 12px at the bottom.
68
- indent && 'mt-1.5', indent), children: [copyText && (_jsx(CopyButton, { code: copyText, className: "text-muted-foreground/60 hover:text-foreground absolute top-1 right-1 z-10" })), _jsx("div", { "data-scrollable": true, className: cn('max-h-96 overflow-auto', pad, 'pr-11'), children: children })] }));
72
+ indent && 'mt-1.5', indent), children: [copyText && (_jsx(CopyButton, { code: copyText, className: "text-muted-foreground hover:text-foreground absolute top-1 right-1 z-10" })), _jsx("div", { "data-scrollable": true, className: cn('max-h-96 overflow-auto', pad, 'pr-11'), children: children })] }));
69
73
  }
70
74
  /**
71
75
  * Raw tool output as its own object: a muted, hairlined card with a copy
@@ -89,5 +93,5 @@ export function RawOutputBlock({ output, maxChars = 2000 }) {
89
93
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
90
94
  const { text, truncatedChars } = useMemo(() => formatRawOutput(output, maxChars), [output, maxChars]);
91
95
  const isMarkdown = useMemo(() => looksLikeMarkdown(text), [text]);
92
- return (_jsxs(ToolOutputCard, { copyText: output, children: [isMarkdown ? (_jsx("div", { className: cn('text-sm', MD_FLUSH_CLASSES), children: _jsx(UnifiedMarkdown, { content: text, trust: "untrusted" }) })) : (_jsx("pre", { className: "text-muted-foreground font-mono text-xs leading-relaxed wrap-break-word whitespace-pre-wrap", children: text })), truncatedChars > 0 && (_jsxs("div", { className: "text-muted-foreground/40 mt-2 text-xs", children: ["+", truncatedChars.toLocaleString(), " ", tI18nComplete.raw('text6d0123a90793')] }))] }));
96
+ return (_jsxs(ToolOutputCard, { copyText: output, children: [isMarkdown ? (_jsx("div", { className: cn('text-sm', MD_FLUSH_CLASSES), children: _jsx(UnifiedMarkdown, { content: text, trust: "untrusted" }) })) : (_jsx("pre", { className: "text-muted-foreground font-mono text-xs leading-relaxed wrap-break-word whitespace-pre-wrap", children: text })), truncatedChars > 0 && (_jsxs("div", { className: "text-muted-foreground mt-2 text-xs", children: ["+", truncatedChars.toLocaleString(), " ", tI18nComplete.raw('text6d0123a90793')] }))] }));
93
97
  }
@@ -120,7 +120,15 @@ export function PanelRowTitle({ trigger, running, onSubtitleClick, }) {
120
120
  const open = useToolOpen();
121
121
  const subtitle = open && trigger.hideSubtitleWhenOpen ? undefined : trigger.subtitle;
122
122
  return (_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden", children: [running ? (_jsx(TextShimmer, { className: cn(PANEL_TITLE_CLASS, 'text-sm font-medium'), children: trigger.title || 'Working' })) : (_jsx("span", { className: cn('text-foreground text-sm font-medium', PANEL_TITLE_CLASS), children: trigger.title })), subtitle && (_jsx("span", { className: cn('text-muted-foreground min-w-0 truncate font-mono text-xs', onSubtitleClick &&
123
- 'hover:text-foreground cursor-pointer underline-offset-2 hover:underline'), title: subtitle,
123
+ 'hover:text-foreground cursor-pointer underline-offset-2 hover:underline'), role: onSubtitleClick ? 'button' : undefined, tabIndex: onSubtitleClick ? 0 : undefined, title: subtitle, onKeyDown: onSubtitleClick
124
+ ? (e) => {
125
+ if (e.key !== 'Enter' && e.key !== ' ')
126
+ return;
127
+ e.preventDefault();
128
+ e.stopPropagation();
129
+ onSubtitleClick();
130
+ }
131
+ : undefined,
124
132
  // `stopPropagation` is load-bearing now and was not before: the
125
133
  // subtitle sits INSIDE the disclosure trigger, so without it every
126
134
  // "open this file" click would also toggle the row it lives on.
@@ -4,7 +4,7 @@ import { Disclosure, DisclosureContent, DisclosureTrigger } from '../../../primi
4
4
  import { SidebarGlyph as PanelRight } from '../../../icons/sidebar-toggle.js';
5
5
  import { CaretRightIcon } from '@phosphor-icons/react';
6
6
  import { cn } from '../../../lib/utils.js';
7
- import { useCallback, useContext, useEffect, useState } from 'react';
7
+ import { useCallback, useContext, useState } from 'react';
8
8
  import { ToolSurfaceContext } from './surface.js';
9
9
  import { BoundActivateContext, ToolDurationContext, ToolOpenContext, ToolOutcomeContext, ToolRunningContext } from './infrastructure-contexts.js';
10
10
  import { PanelRowTitle, TOOL_ROW_CLASS, ToolHeaderRow, ToolOutcomeIcon } from './infrastructure-row-header.js';
@@ -110,13 +110,15 @@ export function BasicTool({ icon, trigger, children, defaultOpen = false, forceO
110
110
  // could only open the row on the frame AFTER mount, so a call that arrives
111
111
  // already waiting on a permission or a question rendered closed once and then
112
112
  // snapped open — a flash of the wrong answer on the exact row whose prompt is
113
- // the point. The effect stays for the case it is actually for: `forceOpen`
114
- // flipping true on an already-mounted row.
113
+ // the point. The render-time latch below covers `forceOpen` flipping true on
114
+ // an already-mounted row, in the same render.
115
115
  const [open, setOpen] = useState(defaultOpen || !!forceOpen);
116
- useEffect(() => {
116
+ const [prevForceOpen, setPrevForceOpen] = useState(forceOpen);
117
+ if (forceOpen !== prevForceOpen) {
118
+ setPrevForceOpen(forceOpen);
117
119
  if (forceOpen)
118
120
  setOpen(true);
119
- }, [forceOpen]);
121
+ }
120
122
  const handleOpenChange = useCallback((value) => {
121
123
  if (locked && !value)
122
124
  return;