@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
@@ -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;
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { HighlightedCode } from '../../../markdown/code/index.js';
4
4
  import { TextShimmer } from '../../../primitives/text-shimmer.js';
5
- import { BasicTool, partInput, partMetadata, partOutput, partStatus, partStreamingInput, StructuredOutput, ToolRunningContext, useToolCardFrame, useToolCardPad, useToolIndent, useToolOpen, partRawOutput, } from '../shared/infrastructure.js';
5
+ import { BasicTool, partInput, partMetadata, partOutput, partStatus, partStreamingInput, StructuredOutput, RecoveredCallsContext, ToolRunningContext, useToolCardFrame, useToolCardPad, useToolIndent, useToolOpen, partRawOutput, } from '../shared/infrastructure.js';
6
6
  import { GenericTool } from '../generic-tool.js';
7
7
  import { ToolRegistry } from '../shared/registry.js';
8
8
  import { cn } from '../../../lib/utils.js';
@@ -140,7 +140,7 @@ export function dedentCommand(raw) {
140
140
  * The exit-code strip keeps its shorter 8px vertical inset: it is metadata
141
141
  * about the card, not a third pane of content.
142
142
  */
143
- function CommandBlock({ command, output, richOutput, exitCode, settled, }) {
143
+ function CommandBlock({ command, output, richOutput, exitCode, recovered = false, settled, }) {
144
144
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
145
145
  const hasOutput = Boolean(richOutput || output);
146
146
  const failed = typeof exitCode === 'number' && exitCode !== 0;
@@ -161,7 +161,7 @@ function CommandBlock({ command, output, richOutput, exitCode, settled, }) {
161
161
  _jsxs("div", { className: cn('relative overflow-hidden', frame), children: [_jsx("div", { "data-scrollable": true, className: cn('max-h-64 overflow-auto', frame && 'bg-muted/40'), children: _jsx("pre", { className: cn('text-foreground font-mono text-xs leading-relaxed wrap-break-word whitespace-pre-wrap [&_code]:border-none [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-xs [&_code]:leading-relaxed [&_code]:whitespace-pre-wrap [&_pre]:whitespace-pre-wrap [&_span]:border-none [&_span]:outline-none', paneInset, 'pr-11'), children: _jsx(HighlightedCode, { code: command, language: "bash", children: command }) }) }), _jsx("div", { className: "absolute top-2 right-2", children: _jsx(CopyButton, { code: command, className: "text-muted-foreground hover:text-foreground" }) }), (hasOutput || settled) && (_jsx("div", { className: "border-border/60 border-t", children: richOutput ? (richOutput) : output ? (_jsxs("div", { className: "relative", children: [_jsx("div", { "data-scrollable": true, className: "max-h-80 overflow-auto", children: _jsx("div", { className: cn('text-muted-foreground font-mono text-xs leading-relaxed wrap-break-word whitespace-pre-wrap', paneInset, 'pr-11'), children: output }) }), _jsx("div", { className: "absolute top-2 right-2", children: _jsx(CopyButton, { code: output, className: "text-muted-foreground hover:text-foreground" }) })] })) : (_jsx("p", { className: cn('text-muted-foreground', paneInset, 'text-xs leading-relaxed'), children: tI18nComplete.raw('textf7e31759b202') })) })), failed && (_jsxs("div", { className: cn(
162
162
  // `kortix-red`, matching the trigger's "Command failed": the strip
163
163
  // is the card's own copy of the verdict, so the two agree in tone.
164
- 'border-border/60 text-kortix-red border-t py-2 font-mono text-xs tabular-nums',
164
+ 'border-border/60 border-t py-2 font-mono text-xs tabular-nums', recovered ? 'text-muted-foreground' : 'text-kortix-red',
165
165
  // Metadata about the card, not a third pane: it keeps the shorter
166
166
  // 8px vertical inset, and only the horizontal one is the surface's
167
167
  // to supply.
@@ -244,9 +244,13 @@ export function BashTool({ call, defaultOpen, forceOpen, locked }) {
244
244
  const exitCode = useMemo(() => {
245
245
  if (call.status !== 'complete')
246
246
  return undefined;
247
- return shellExitCode(partRawOutput(call));
247
+ // OpenCode tags the code in the output; pi records it beside the output (KRTX-3021).
248
+ const recorded = partMetadata(call).exit;
249
+ return shellExitCode(partRawOutput(call)) ?? (typeof recorded === 'number' ? recorded : undefined);
248
250
  }, [call]);
249
251
  const failed = typeof exitCode === 'number' && exitCode !== 0;
252
+ // A failure the agent moved past keeps its words, not the red (KRTX-3021).
253
+ const recovered = useContext(RecoveredCallsContext).has(call.id);
250
254
  // `teams send "…"` / `slack send …` / `telegram send …` IS the reply the
251
255
  // person in the channel received. It renders as that message — badge, text,
252
256
  // attachment — with the shell call kept underneath for the record. A failed
@@ -268,12 +272,12 @@ export function BashTool({ call, defaultOpen, forceOpen, locked }) {
268
272
  if (!command && toolKind(call.name) !== 'bash') {
269
273
  return _jsx(GenericTool, { call: call, defaultOpen: defaultOpen, forceOpen: forceOpen, locked: locked });
270
274
  }
271
- return (_jsxs(BasicTool, { icon: send ? (_jsx(ChannelBrandMark, { platform: send.platform, className: "size-4 shrink-0" })) : (_jsx(TerminalIcon, { className: "size-4 shrink-0" })), trigger: isStalePending ? (_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: _jsx(TextShimmer, { duration: 1, spread: 2, children: tI18nComplete.raw('textb93900bded31') }) })) : commandPreview ? (_jsx(BashTrigger, { title: title, failed: failed, command: command, commandPreview: send ? (sendText ?? send.file ?? '') : commandPreview, extraLines: send ? 0 : extraLines, live: running && status !== 'completed' && status !== 'error' })) : null,
275
+ return (_jsxs(BasicTool, { icon: send ? (_jsx(ChannelBrandMark, { platform: send.platform, className: "size-4 shrink-0" })) : (_jsx(TerminalIcon, { className: "size-4 shrink-0" })), trigger: isStalePending ? (_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: _jsx(TextShimmer, { duration: 1, spread: 2, children: tI18nComplete.raw('textb93900bded31') }) })) : commandPreview ? (_jsx(BashTrigger, { title: title, failed: failed && !recovered, command: command, commandPreview: send ? (sendText ?? send.file ?? '') : commandPreview, extraLines: send ? 0 : extraLines, live: running && status !== 'completed' && status !== 'error' })) : null,
272
276
  // The reply is the point of the row, so it opens showing it.
273
277
  defaultOpen: defaultOpen || Boolean(send), forceOpen: forceOpen, locked: locked, children: [send && (_jsx("div", { className: cn('mb-1.5', indent && 'mt-1.5', indent), children: _jsx(ChannelSendCard, { send: send }) })), command && (
274
278
  // `CommandBlock` is a bordered card like the shared three, so it takes
275
279
  // the same gate: the seam belongs to the inline row it hangs under, not
276
280
  // to the panel body that already supplies `px-3 py-3`.
277
- _jsx("div", { className: cn(indent && 'mt-1.5', indent), children: _jsx(CommandBlock, { command: command, output: plainOutput, richOutput: richOutput, exitCode: exitCode, settled: status === 'completed' || status === 'error' }) }))] }));
281
+ _jsx("div", { className: cn(indent && 'mt-1.5', indent), children: _jsx(CommandBlock, { command: command, output: plainOutput, richOutput: richOutput, exitCode: exitCode, recovered: recovered, settled: status === 'completed' || status === 'error' }) }))] }));
278
282
  }
279
283
  ToolRegistry.register('bash', BashTool);
@@ -1,13 +1,19 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { BasicTool, isErrorOutput, partInput, partOutput, partStatus, ToolOutputFallback, partRawOutput, } from '../shared/infrastructure.js';
4
+ import { JsonFailureOutputCard } from '../shared/infrastructure-output.js';
4
5
  import { ToolRegistry } from '../shared/registry.js';
5
6
  import { ToolResultCard } from '../shared/result-card.js';
6
7
  import { WebSourceRow } from '../shared/web-source-row.js';
7
8
  import { MagnifyingGlassIcon as Search } from '@phosphor-icons/react';
8
9
  import { useMemo } from 'react';
9
10
  import { humanizeSearchQuery } from '@kortix/sdk';
10
- import { parseWebSearchOutput, } from '@kortix/sdk';
11
+ import { parseJsonFailure, parseWebSearchOutput, } from '@kortix/sdk';
12
+ /** A failed query's error as the shared failure card reads it: a nested JSON
13
+ * message (a provider's "over plan quota") becomes its own line. */
14
+ function queryFailure(error) {
15
+ return parseJsonFailure(JSON.stringify({ success: false, error })) ?? { errorSummary: error };
16
+ }
11
17
  /**
12
18
  * Every source, flat, inside one bordered card.
13
19
  *
@@ -26,8 +32,24 @@ import { parseWebSearchOutput, } from '@kortix/sdk';
26
32
  * get a muted one-line query caption between segments — a caption, not a
27
33
  * control. No accordions, no "show more" button.
28
34
  */
35
+ /** The one error every query of a batch failed with, or null. A provider
36
+ * limit fails them all alike, and five copies of it read as five problems. */
37
+ function sharedFailure(queryResults) {
38
+ const first = queryResults[0]?.error;
39
+ if (queryResults.length < 2 || !first)
40
+ return null;
41
+ return queryResults.every((qr) => qr.error === first && qr.sources.length === 0) ? first : null;
42
+ }
43
+ function QueryCaption({ query }) {
44
+ return (_jsxs("div", { className: "text-muted-foreground flex items-center gap-2 px-2 pt-2 pb-1 text-xs", children: [_jsx(Search, { className: "size-3 shrink-0" }), _jsx("span", { className: "truncate", children: humanizeSearchQuery(query) })] }));
45
+ }
29
46
  function FlatSourceList({ queryResults }) {
30
- return (_jsx(ToolResultCard, { children: queryResults.map((qr, qi) => (_jsxs("div", { children: [queryResults.length > 1 && qr.sources.length > 0 && (_jsxs("div", { className: "text-muted-foreground/70 flex items-center gap-2 px-2 pt-2 pb-1 text-xs", children: [_jsx(Search, { className: "size-3 shrink-0" }), _jsx("span", { className: "truncate", children: humanizeSearchQuery(qr.query) })] })), qr.sources.map((src) => (_jsx(WebSourceRow, { url: src.url, title: src.title || src.url }, src.url)))] }, qi))) }));
47
+ const shared = sharedFailure(queryResults);
48
+ if (shared) {
49
+ // The failure once, then the searches it stopped (KRTX-3022).
50
+ return (_jsxs(ToolResultCard, { children: [_jsx(JsonFailureOutputCard, { failure: queryFailure(shared) }), queryResults.map((qr, qi) => (_jsx(QueryCaption, { query: qr.query }, qi)))] }));
51
+ }
52
+ return (_jsx(ToolResultCard, { children: queryResults.map((qr, qi) => (_jsxs("div", { children: [queryResults.length > 1 && (qr.sources.length > 0 || qr.error) && _jsx(QueryCaption, { query: qr.query }), qr.error && _jsx(JsonFailureOutputCard, { failure: queryFailure(qr.error) }), qr.sources.map((src) => (_jsx(WebSourceRow, { url: src.url, title: src.title || src.url }, src.url)))] }, qi))) }));
31
53
  }
32
54
  export function WebSearchTool({ call, defaultOpen, forceOpen, locked }) {
33
55
  const input = partInput(call);
@@ -58,7 +80,7 @@ export function WebSearchTool({ call, defaultOpen, forceOpen, locked }) {
58
80
  const triggerBadge = status === 'completed' && !isError && totalSources > 0
59
81
  ? `${totalSources} ${totalSources === 1 ? 'result' : 'results'}`
60
82
  : undefined;
61
- return (_jsx(BasicTool, { icon: _jsx(Search, { className: "size-3.5 shrink-0" }), trigger: _jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [_jsx("span", { className: "text-foreground min-w-0 truncate text-sm", children: triggerLabel }), triggerBadge && (_jsx("span", { className: "text-muted-foreground/70 ml-auto shrink-0 text-sm whitespace-nowrap", children: triggerBadge }))] }), defaultOpen: defaultOpen, forceOpen: forceOpen, locked: locked, children: _jsx(_Fragment, { children: isError ? (_jsx(ToolOutputFallback, { output: output, toolName: "web_search" })) : queryResults.length > 0 ? (_jsx(FlatSourceList, { queryResults: queryResults })) : output ? (_jsx(ToolOutputFallback, { output: output, isStreaming: status === 'running', toolName: "web_search" })) : null }) }));
83
+ return (_jsx(BasicTool, { icon: _jsx(Search, { className: "size-3.5 shrink-0" }), trigger: _jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [_jsx("span", { className: "text-foreground min-w-0 truncate text-sm", children: triggerLabel }), triggerBadge && (_jsx("span", { className: "text-muted-foreground ml-auto shrink-0 text-sm whitespace-nowrap", children: triggerBadge }))] }), defaultOpen: defaultOpen, forceOpen: forceOpen, locked: locked, children: _jsx(_Fragment, { children: isError ? (_jsx(ToolOutputFallback, { output: output, toolName: "web_search" })) : queryResults.length > 0 ? (_jsx(FlatSourceList, { queryResults: queryResults })) : output ? (_jsx(ToolOutputFallback, { output: output, isStreaming: status === 'running', toolName: "web_search" })) : null }) }));
62
84
  }
63
85
  ToolRegistry.register('websearch', WebSearchTool);
64
86
  ToolRegistry.register('web-search', WebSearchTool);
@@ -0,0 +1,8 @@
1
+ import { type SessionLogBlock } from '@kortix/sdk';
2
+ /**
3
+ * The failed commands of a turn that a later step worked past: a call after them
4
+ * completed and did not fail. A probe such as `ls missing-dir` exits non-zero by
5
+ * design, and the agent carries on; only a failure the turn ended on is still
6
+ * the news (KRTX-3021).
7
+ */
8
+ export declare function recoveredShellFailures(blocks: readonly SessionLogBlock[]): Set<string>;
@@ -0,0 +1,32 @@
1
+ import { shellExitCode, toolCallMetadata, toolCallRawOutput, } from '@kortix/sdk';
2
+ /** A shell command that ran and exited non-zero, by either harness's record of its code. */
3
+ function failedCommand(block) {
4
+ if (block.name !== 'bash' || block.status !== 'complete')
5
+ return false;
6
+ const recorded = toolCallMetadata(block).exit;
7
+ const exit = shellExitCode(toolCallRawOutput(block)) ?? (typeof recorded === 'number' ? recorded : undefined);
8
+ return typeof exit === 'number' && exit !== 0;
9
+ }
10
+ /**
11
+ * The failed commands of a turn that a later step worked past: a call after them
12
+ * completed and did not fail. A probe such as `ls missing-dir` exits non-zero by
13
+ * design, and the agent carries on; only a failure the turn ended on is still
14
+ * the news (KRTX-3021).
15
+ */
16
+ export function recoveredShellFailures(blocks) {
17
+ const recovered = new Set();
18
+ const pending = [];
19
+ for (const block of blocks) {
20
+ if (block.type !== 'tool_call')
21
+ continue;
22
+ if (failedCommand(block)) {
23
+ pending.push(block.id);
24
+ }
25
+ else if (block.status === 'complete') {
26
+ for (const id of pending)
27
+ recovered.add(id);
28
+ pending.length = 0;
29
+ }
30
+ }
31
+ return recovered;
32
+ }
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  /**
4
4
  * Streamed text reaches the client in network chunks of any size and at any
5
5
  * spacing: one token every 16 ms, or a paragraph after a 300 ms stall.
@@ -287,14 +287,11 @@ export function useStreamingCadence(rawValue, active, revealKey, clock = default
287
287
  const resume = kept && clock.now() - kept.at < RESUME_WINDOW_MS && kept.len < value.length;
288
288
  return { text: resume ? value.slice(0, kept.len) : value, streaming: active };
289
289
  });
290
- const pacerRef = useRef(null);
291
- if (pacerRef.current === null) {
292
- pacerRef.current = createStreamPacer((text, streaming) => setState({ text, streaming }), state.text, clock, state.streaming);
293
- }
290
+ const [pacer] = useState(() => createStreamPacer((text, streaming) => setState({ text, streaming }), state.text, clock, state.streaming));
294
291
  useEffect(() => {
295
- pacerRef.current?.push(value, active);
296
- }, [value, active]);
297
- useEffect(() => () => pacerRef.current?.dispose(), []);
292
+ pacer.push(value, active);
293
+ }, [pacer, value, active]);
294
+ useEffect(() => () => pacer.dispose(), [pacer]);
298
295
  useEffect(() => {
299
296
  if (revealKey && state.streaming)
300
297
  rememberReveal(revealKey, state.text.length, clock.now());