@jini-ai/chat 0.6.15-rc.2 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/core/accepted-input.js +20 -11
- package/dist/core/ag-ui/entry.js +3 -1
- package/dist/core/agent-plugin-chat-state.js +3 -1
- package/dist/core/antigravity/tool-presentation.d.ts +13 -0
- package/dist/core/antigravity/tool-presentation.js +20 -0
- package/dist/core/assistant-client/ag-ui-transport.js +14 -2
- package/dist/core/assistant-client/attachment-liveness.js +1 -1
- package/dist/core/assistant-client/durable-run-subscription.js +2 -1
- package/dist/core/assistant-client/transport.js +13 -7
- package/dist/core/assistant-content.js +2 -1
- package/dist/core/composer-history.js +2 -1
- package/dist/core/durable-projection.js +2 -1
- package/dist/core/durable-runs/continuation.js +2 -1
- package/dist/core/events.d.ts +5 -1
- package/dist/core/messages.d.ts +5 -1
- package/dist/core/partial-json.js +18 -9
- package/dist/core/persistence/title.d.ts +1 -1
- package/dist/core/persistence/title.js +25 -7
- package/dist/core/question-form/normalize.js +1 -1
- package/dist/core/question-form/scan.js +25 -15
- package/dist/core/question-form/secret-answers.js +2 -1
- package/dist/core/run-activity.js +8 -4
- package/dist/core/run-events/entry.js +6 -2
- package/dist/core/tool-events.d.ts +18 -8
- package/dist/core/tool-events.js +72 -16
- package/dist/core/tool-presentation.d.ts +14 -0
- package/dist/core/tool-presentation.js +8 -0
- package/dist/core/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +4 -6
- package/dist/core/user-text-redaction.js +13 -6
- package/dist/core/util/manifest.js +13 -11
- package/dist/core/util/markdown-context.d.ts +14 -0
- package/dist/core/util/markdown-context.js +42 -4
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.js +7 -7
- package/dist/core/util/pointer.js +22 -6
- package/dist/core/util/recover.js +5 -8
- package/dist/core/util/strip.js +12 -15
- package/dist/core/util/validate.js +30 -18
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
- package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
- package/dist/embed-widget/browser/lazy-loader.js +16 -12
- package/dist/embed-widget/browser/session-store.d.ts +3 -9
- package/dist/embed-widget/browser/session-store.js +6 -38
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.d.ts +3 -10
- package/dist/embed-widget/react/embed.js +8 -52
- package/dist/embed-widget/react/header.hooks.js +1 -1
- package/dist/embed-widget/transports/fetch-sse.js +2 -1
- package/dist/embed-widget/vanilla/index.d.ts +0 -5
- package/dist/embed-widget/vanilla/index.js +23 -26
- package/dist/embed-widget/vanilla/state.d.ts +3 -11
- package/dist/embed-widget/vanilla/state.js +10 -39
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
- package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
- package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
- package/dist/react/components/ChatSettingsDialog.js +18 -9
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +5 -4
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +1 -0
- package/dist/react/components/Markdown.d.ts +1 -0
- package/dist/react/components/Markdown.js +30 -27
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.d.ts +3 -0
- package/dist/react/components/MessageRow.js +32 -14
- package/dist/react/components/MessageRow.usage.js +3 -1
- package/dist/react/components/QuestionForm.js +4 -5
- package/dist/react/components/SentAttachment.js +4 -1
- package/dist/react/components/TodoCard.js +9 -1
- package/dist/react/components/ToolCard.d.ts +3 -1
- package/dist/react/components/ToolCard.js +84 -64
- package/dist/react/components/ToolCard.rules.d.ts +20 -0
- package/dist/react/components/ToolCard.rules.js +92 -0
- package/dist/react/components/composer-discovery.js +8 -8
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +31 -9
- package/dist/react/features/chat-pane/components/ChatPane.js +7 -5
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +13 -5
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
- package/dist/react/features/chat-pane/model-options.js +4 -2
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +95 -5
- package/dist/react/features/chat-pane/types.d.ts +11 -0
- package/dist/react/hooks/assistant-chats.js +22 -8
- package/dist/react/hooks/useArtifactStream.js +2 -1
- package/dist/react/hooks/useComposer.js +2 -2
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
- package/dist/react/hooks/useConversation.js +9 -4
- package/dist/react/hooks/useExtEventGroups.js +4 -2
- package/dist/react/hooks/useLatestOperation.js +2 -1
- package/dist/react/hooks/useQuestionForms.js +32 -22
- package/dist/react/hooks/useRunStream.js +4 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
- package/dist/react/hooks/useUserTextGuard.js +3 -1
- package/dist/react/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- package/dist/react/tool-call-display.d.ts +11 -0
- package/dist/react/tool-call-display.js +59 -2
- package/dist/server/durable-runs/recover.js +13 -5
- package/dist/server/run-finalizer/entry.js +1 -0
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.js +34 -24
- package/dist/store/runtime/run-ledger.js +7 -4
- package/dist/store/sql/boundary.js +3 -1
- package/dist/store/sql/cursor.js +8 -3
- package/dist/store/sql/store.js +36 -28
- package/package.json +93 -88
- package/dist/core/browser/page-actions.d.ts +0 -121
- package/dist/core/browser/page-actions.js +0 -115
- package/dist/embed-widget/browser/page-actions.d.ts +0 -2
- package/dist/embed-widget/browser/page-actions.js +0 -3
|
@@ -427,6 +427,13 @@ export const CHAT_PANE_STYLES = `
|
|
|
427
427
|
border-radius: 8px;
|
|
428
428
|
font-size: 12px;
|
|
429
429
|
}
|
|
430
|
+
.jini-chat-pane__status--dismissible {
|
|
431
|
+
display: flex;
|
|
432
|
+
align-items: flex-start;
|
|
433
|
+
justify-content: space-between;
|
|
434
|
+
gap: 10px;
|
|
435
|
+
}
|
|
436
|
+
.jini-chat-pane__status--dismissible > button { flex: none; }
|
|
430
437
|
.jini-chat-pane__drop-target {
|
|
431
438
|
position: relative;
|
|
432
439
|
margin: -8px -8px -8px;
|
|
@@ -717,10 +724,15 @@ export const CHAT_PANE_STYLES = `
|
|
|
717
724
|
.jini-chat-pane.jini-chat-pane .jini-composer-input { font-size: 16px; }
|
|
718
725
|
}
|
|
719
726
|
.jini-chat-pane .jini-composer-input::placeholder { color: var(--jini-chat-faint); }
|
|
727
|
+
/* Wraps as a last resort: the leading slot gives way first (below), then the model label truncates,
|
|
728
|
+
and only then does the trailing group move to its own line whole. Unwrapped, a picked-folder chip
|
|
729
|
+
in the leading slot squeezed the model picker until send sat on its label (owner report,
|
|
730
|
+
2026-10-29); wrapping on the group's full width put picker and send on a second row whenever the
|
|
731
|
+
untruncated labels did not fit (owner, 2026-10-30: one line). */
|
|
720
732
|
.jini-chat-pane .jini-composer-footer {
|
|
721
733
|
display: flex;
|
|
734
|
+
flex-wrap: wrap;
|
|
722
735
|
align-items: center;
|
|
723
|
-
justify-content: space-between;
|
|
724
736
|
min-height: 42px;
|
|
725
737
|
gap: 6px;
|
|
726
738
|
padding: 6px 8px 8px;
|
|
@@ -730,7 +742,14 @@ export const CHAT_PANE_STYLES = `
|
|
|
730
742
|
/* Host controls slotted at the start of the footer row, right after the attach/discovery "+"
|
|
731
743
|
trigger above -- see ComposerSlots.footerLeadingAccessory's own doc (slots.ts) for why this
|
|
732
744
|
exists as a third footer slot alongside footerAccessories/plusMenuItems. */
|
|
733
|
-
|
|
745
|
+
/* Basis 0, so a line break is decided on the slot's min-content, not its full width: a chip in it
|
|
746
|
+
truncates before the trailing group wraps. It grows back to its content, never past it. */
|
|
747
|
+
.jini-chat-pane .jini-composer-footer-leading {
|
|
748
|
+
display: inline-flex;
|
|
749
|
+
flex: 1 1 0%;
|
|
750
|
+
align-items: center;
|
|
751
|
+
max-width: max-content;
|
|
752
|
+
}
|
|
734
753
|
/* NOT 'position: relative' — the discovery popover (below) anchors off '.jini-composer' itself
|
|
735
754
|
(the nearest positioned ancestor once this wrapper opts out), not off this small trigger-button
|
|
736
755
|
wrapper. Anchoring to the wrapper put the popover's bottom edge at the wrapper's own top edge —
|
|
@@ -951,9 +970,21 @@ export const CHAT_PANE_STYLES = `
|
|
|
951
970
|
color: var(--jini-chat-muted);
|
|
952
971
|
font-size: 12px;
|
|
953
972
|
}
|
|
973
|
+
/* Picker, plus menu and send: one group at the trailing edge that wraps as a unit. Basis 0 with an
|
|
974
|
+
automatic minimum, like the leading slot: the line break is decided on the group's min-content
|
|
975
|
+
(model label truncated, below), so the label gives way before the group wraps. It grows back to
|
|
976
|
+
its content, never past it. */
|
|
977
|
+
.jini-chat-pane .jini-composer-footer-trailing {
|
|
978
|
+
display: flex;
|
|
979
|
+
flex: 1 1 0%;
|
|
980
|
+
align-items: center;
|
|
981
|
+
gap: 6px;
|
|
982
|
+
max-width: max-content;
|
|
983
|
+
margin-left: auto;
|
|
984
|
+
}
|
|
954
985
|
/* Always pushed to the trailing edge. This also covers a footer with no attach/accessory controls
|
|
955
986
|
before the send group (a public visitor chat), which would otherwise leave the lone send button
|
|
956
|
-
at the start
|
|
987
|
+
at the start of the row. */
|
|
957
988
|
.jini-chat-pane .jini-composer-send-group {
|
|
958
989
|
display: flex;
|
|
959
990
|
flex-shrink: 0;
|
|
@@ -1177,6 +1208,9 @@ export const CHAT_PANE_STYLES = `
|
|
|
1177
1208
|
cursor: pointer;
|
|
1178
1209
|
}
|
|
1179
1210
|
.jini-runtime-trigger { gap: 6px; padding: 3px 4px; }
|
|
1211
|
+
/* The model label truncates, but the trigger keeps room for its chevron and a few characters;
|
|
1212
|
+
at the shared 'min-width: 0' it shrank to 12px and the chevron spilled onto the effort label. */
|
|
1213
|
+
.jini-runtime-picker .jini-runtime-dropup-trigger { min-width: 4.5em; }
|
|
1180
1214
|
/* Local mode's trigger is the icon alone, so it has nothing to give way: shrunk under the shared
|
|
1181
1215
|
\`min-width: 0\` on a narrow bar, the 22px icon spilled over the model/effort labels beside it
|
|
1182
1216
|
(owner report, 2026-10-21). API mode keeps shrinking -- its model label ellipsizes. */
|
|
@@ -1206,6 +1240,16 @@ export const CHAT_PANE_STYLES = `
|
|
|
1206
1240
|
text-overflow: ellipsis;
|
|
1207
1241
|
white-space: nowrap;
|
|
1208
1242
|
}
|
|
1243
|
+
/* In the composer footer: one clamped line that may break anywhere, so its min-content is one
|
|
1244
|
+
character and the trailing group's min-content (\`.jini-composer-footer-trailing\`) counts the label
|
|
1245
|
+
at the trigger's 4.5em floor, not its full text. */
|
|
1246
|
+
.jini-chat-pane .jini-composer-footer-trailing .jini-runtime-dropup-trigger__label {
|
|
1247
|
+
display: -webkit-box;
|
|
1248
|
+
-webkit-box-orient: vertical;
|
|
1249
|
+
-webkit-line-clamp: 1;
|
|
1250
|
+
white-space: normal;
|
|
1251
|
+
word-break: break-all;
|
|
1252
|
+
}
|
|
1209
1253
|
.jini-runtime-trigger__chevron { flex: none; }
|
|
1210
1254
|
/* The effort label is short; the model label is the one that gives way on a narrow bar. */
|
|
1211
1255
|
.jini-runtime-dropup-trigger--effort { flex: none; }
|
|
@@ -1367,6 +1411,26 @@ export const CHAT_PANE_STYLES = `
|
|
|
1367
1411
|
text-overflow: ellipsis;
|
|
1368
1412
|
white-space: nowrap;
|
|
1369
1413
|
}
|
|
1414
|
+
/* An agent's full-access note (FullAccessNoteTip): a danger-tinted warning icon whose InfoTip holds
|
|
1415
|
+
the text, in place of the paragraph that squeezed the composer row. 24px hit target; flex: none
|
|
1416
|
+
so it never shrinks or wraps the row. The popover is portalled out of .jini-chat-pane, hence the
|
|
1417
|
+
fallback colour. */
|
|
1418
|
+
.jini-runtime-access-note { display: inline-flex; flex: none; }
|
|
1419
|
+
.jini-runtime-access-note .info-tip { margin-left: 0; }
|
|
1420
|
+
.jini-runtime-access-note .info-tip-icon,
|
|
1421
|
+
.jini-runtime-access-note .info-tip:hover .info-tip-icon,
|
|
1422
|
+
.jini-runtime-access-note .info-tip:focus-within .info-tip-icon {
|
|
1423
|
+
width: 24px;
|
|
1424
|
+
height: 24px;
|
|
1425
|
+
color: var(--jini-chat-danger, #e5484d);
|
|
1426
|
+
border-radius: 6px;
|
|
1427
|
+
}
|
|
1428
|
+
.jini-runtime-access-note .info-tip-icon:hover { background: var(--jini-chat-subtle); }
|
|
1429
|
+
.jini-runtime-access-note .info-tip-icon:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; border-radius: 6px; }
|
|
1430
|
+
/* The note's icon sits beside its radio, not inside it (a control may not nest in a button). */
|
|
1431
|
+
.jini-runtime-agent-row { display: flex; align-items: center; }
|
|
1432
|
+
.jini-runtime-agent-row > .jini-runtime-agent { flex: 1; min-width: 0; }
|
|
1433
|
+
.jini-runtime-agent-row > .jini-runtime-access-note { margin-right: 4px; }
|
|
1370
1434
|
.jini-runtime-empty { padding: 10px 11px; color: var(--jini-chat-muted); font-size: 12px; }
|
|
1371
1435
|
.jini-runtime-models {
|
|
1372
1436
|
display: flex;
|
|
@@ -1453,8 +1517,22 @@ export const CHAT_PANE_STYLES = `
|
|
|
1453
1517
|
.jini-chat-pane .op-status { display: inline-flex; flex-shrink: 0; }
|
|
1454
1518
|
.jini-chat-pane .op-status-ok { color: #4e875f; }
|
|
1455
1519
|
.jini-chat-pane .op-status-error { color: var(--danger); }
|
|
1520
|
+
.jini-chat-pane .op-status-retried { color: var(--jini-chat-muted); }
|
|
1456
1521
|
.jini-chat-pane .op-status-running { color: var(--jini-chat-accent); }
|
|
1457
|
-
|
|
1522
|
+
/* The name truncates before the target does (owner bug 11-01: "Content Read Content…" squeezed the
|
|
1523
|
+
target out — the target was flex: 1 with a 0 basis beside a name that would not shrink). A generic
|
|
1524
|
+
row's target (.op-target, ToolCard's ToolRowHead) keeps its own width up to 35% of the row; both
|
|
1525
|
+
carry their full text as a title. Family cards keep .op-meta filling the row after a short name.
|
|
1526
|
+
35%, not 40%: on the ~333px dock 40% still cut "Content Post Update" to "Content Post Upd…"
|
|
1527
|
+
(checked in Chrome 11-01); 35% shows the whole name and ~12 characters of target. */
|
|
1528
|
+
.jini-chat-pane .op-title {
|
|
1529
|
+
font-weight: 600;
|
|
1530
|
+
flex: 0 1 auto;
|
|
1531
|
+
min-width: 0;
|
|
1532
|
+
overflow: hidden;
|
|
1533
|
+
text-overflow: ellipsis;
|
|
1534
|
+
white-space: nowrap;
|
|
1535
|
+
}
|
|
1458
1536
|
.jini-chat-pane .shimmer-text { animation: jini-chat-shimmer 1.6s ease-in-out infinite; }
|
|
1459
1537
|
@keyframes jini-chat-shimmer { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
|
|
1460
1538
|
@media (prefers-reduced-motion: reduce) { .jini-chat-pane .shimmer-text { animation: none; opacity: .6; } }
|
|
@@ -1466,7 +1544,19 @@ export const CHAT_PANE_STYLES = `
|
|
|
1466
1544
|
flex: 1;
|
|
1467
1545
|
min-width: 0;
|
|
1468
1546
|
}
|
|
1469
|
-
.jini-chat-pane .op-
|
|
1547
|
+
.jini-chat-pane .op-meta.op-target { flex: 0 0 auto; max-width: 35%; }
|
|
1548
|
+
/* ToolRowHead's "Tool call · " prefix: screen readers still hear it, the ~333px dock row doesn't spend
|
|
1549
|
+
half its width on it (owner bug 11-01). Clipped, never display:none, so it stays in the accessible name. */
|
|
1550
|
+
.jini-chat-pane .op-sr-prefix {
|
|
1551
|
+
position: absolute;
|
|
1552
|
+
width: 1px;
|
|
1553
|
+
height: 1px;
|
|
1554
|
+
overflow: hidden;
|
|
1555
|
+
clip: rect(0, 0, 0, 0);
|
|
1556
|
+
white-space: nowrap;
|
|
1557
|
+
clip-path: inset(50%);
|
|
1558
|
+
}
|
|
1559
|
+
.jini-chat-pane .op-expand-chev { flex-shrink: 0; margin-left: auto; color: var(--jini-chat-faint); }
|
|
1470
1560
|
/* Collapsed by default: 0 max-height clips the inner content, no display:none — keeps the height
|
|
1471
1561
|
transition instead of an instant jump cut. */
|
|
1472
1562
|
.jini-chat-pane .accordion-collapsible { max-height: 0; overflow: hidden; transition: max-height .15s ease; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
|
|
2
|
+
import type { NativeToolPolicyInput } from '@jini-ai/agent-runtime/native-tool-policy';
|
|
2
3
|
import type { CSSProperties, ReactNode, RefObject } from 'react';
|
|
3
4
|
import type { ChatAttachment, ChatMessage } from '@jini-ai/chat';
|
|
4
5
|
import type { ComposerSlots } from '../../slots.js';
|
|
@@ -57,6 +58,10 @@ export interface ChatPaneAgent {
|
|
|
57
58
|
* as `true` — no badge — since there is no signal either way.
|
|
58
59
|
*/
|
|
59
60
|
supportsTools?: boolean;
|
|
61
|
+
/** Native OFF switches can be enforced and audited; absence fails closed when limits exist. */
|
|
62
|
+
supportsNativeToolAccess?: boolean;
|
|
63
|
+
fullCliAccessOnly?: boolean;
|
|
64
|
+
fullCliAccessNote?: string;
|
|
60
65
|
}
|
|
61
66
|
export interface ChatPaneAgentSelection {
|
|
62
67
|
agentId: string;
|
|
@@ -194,6 +199,11 @@ export type DeliverMidRunMessage = (input: {
|
|
|
194
199
|
}) => Promise<MidRunMessageDelivery>;
|
|
195
200
|
export interface ChatPaneProps extends UserTextRedactionOptions {
|
|
196
201
|
transport: ChatTransport;
|
|
202
|
+
/** Host-localized text shown verbatim while the active run is awaiting acceptance.
|
|
203
|
+
* Omit to use the translated `Sending…` label; ignored once sending ends. */
|
|
204
|
+
sendingLabel?: string;
|
|
205
|
+
/** Current effective host limits, used for a warning before the first message is sent. */
|
|
206
|
+
nativeToolPolicy?: NativeToolPolicyInput;
|
|
197
207
|
agents?: readonly ChatPaneAgent[];
|
|
198
208
|
/** Optional host effects for package-owned inventory, rescan, and health polling. */
|
|
199
209
|
runtimeAccess?: ChatPaneRuntimeAccess;
|
|
@@ -401,6 +411,7 @@ export interface ByokRuntimeSummary {
|
|
|
401
411
|
}
|
|
402
412
|
export interface AgentRuntimePickerProps {
|
|
403
413
|
agents: readonly ChatPaneAgent[];
|
|
414
|
+
nativeToolPolicy?: NativeToolPolicyInput;
|
|
404
415
|
value: ChatPaneAgentSelection;
|
|
405
416
|
onChange: (selection: ChatPaneAgentSelection) => void;
|
|
406
417
|
onRescan?: () => void;
|
|
@@ -414,7 +414,8 @@ export function createAssistantChatsClient({ ports }, _options = {}) {
|
|
|
414
414
|
* conversation that no longer exists and every subsequent message is written at a dead row.
|
|
415
415
|
*/
|
|
416
416
|
const refresh = useCallback(async () => {
|
|
417
|
-
|
|
417
|
+
listSeqRef.current += 1;
|
|
418
|
+
const seq = listSeqRef.current;
|
|
418
419
|
// `null` marks a failed read, so the mount-time restore can tell "not listed" from "no list".
|
|
419
420
|
const read = await portRef.current.listConversations({}, {}).catch(() => null);
|
|
420
421
|
const listed = read ?? [];
|
|
@@ -515,12 +516,13 @@ export function createAssistantChatsClient({ ports }, _options = {}) {
|
|
|
515
516
|
* once, already knowing what it holds and what has been persisted.
|
|
516
517
|
*/
|
|
517
518
|
const select = useCallback((id) => {
|
|
518
|
-
|
|
519
|
+
switchSeqRef.current += 1;
|
|
520
|
+
const seq = switchSeqRef.current;
|
|
519
521
|
const commit = (messages) => {
|
|
520
522
|
// A slower load for a conversation the user has since navigated away from must not land.
|
|
521
523
|
if (switchSeqRef.current !== seq)
|
|
522
524
|
return;
|
|
523
|
-
writtenRef.current.set(id, new Set(messages.map(
|
|
525
|
+
writtenRef.current.set(id, new Set(messages.map(savedMessageWriteKey)));
|
|
524
526
|
setInitialMessages(messages);
|
|
525
527
|
commitActiveId(id);
|
|
526
528
|
// A user-initiated switch is exactly when remounting the pane IS the intent.
|
|
@@ -579,7 +581,8 @@ export function createAssistantChatsClient({ ports }, _options = {}) {
|
|
|
579
581
|
* The conversation row is still created either way. That is deliberate — it exists server-side,
|
|
580
582
|
* and abandoning it silently would be worse than leaving an empty chat in the list.
|
|
581
583
|
*/
|
|
582
|
-
|
|
584
|
+
switchSeqRef.current += 1;
|
|
585
|
+
const seq = switchSeqRef.current;
|
|
583
586
|
const conversation = await portRef.current.createConversation({}, {});
|
|
584
587
|
// Both branches insert locally, so both have to invalidate an in-flight `refresh` — otherwise a
|
|
585
588
|
// list snapshot taken before this POST lands overwrites the row that was just added.
|
|
@@ -660,9 +663,10 @@ export function createAssistantChatsClient({ ports }, _options = {}) {
|
|
|
660
663
|
markListMutated();
|
|
661
664
|
// Held until the PATCH settles, so any refresh landing in between re-applies this title
|
|
662
665
|
// instead of trusting a server read that has not caught up yet.
|
|
663
|
-
|
|
666
|
+
renameVersionRef.current += 1;
|
|
667
|
+
const version = renameVersionRef.current;
|
|
664
668
|
pendingRenamesRef.current.set(id, { version, title });
|
|
665
|
-
setConversations((current) =>
|
|
669
|
+
setConversations((current) => renamedConversations({ current, id, title }));
|
|
666
670
|
try {
|
|
667
671
|
await portRef.current.renameConversation({ id, title }, {});
|
|
668
672
|
}
|
|
@@ -685,9 +689,10 @@ export function createAssistantChatsClient({ ports }, _options = {}) {
|
|
|
685
689
|
const existing = pending.get(key);
|
|
686
690
|
if (existing)
|
|
687
691
|
return existing;
|
|
692
|
+
const isDisposed = () => disposedRef.current;
|
|
688
693
|
const write = (async () => {
|
|
689
694
|
await runStubPendingRef.current.get(conversationId)?.get(key);
|
|
690
|
-
return saveWithRetry(portRef.current, conversationId, message,
|
|
695
|
+
return saveWithRetry(portRef.current, conversationId, message, isDisposed);
|
|
691
696
|
})();
|
|
692
697
|
pending.set(key, write);
|
|
693
698
|
void write.then(() => {
|
|
@@ -879,7 +884,7 @@ export function createAssistantChatsClient({ ports }, _options = {}) {
|
|
|
879
884
|
.createConversation({}, {})
|
|
880
885
|
.then((conversation) => {
|
|
881
886
|
markListMutated();
|
|
882
|
-
setConversations((
|
|
887
|
+
setConversations(prependConversation(conversation));
|
|
883
888
|
writtenRef.current.set(conversation.id, new Set());
|
|
884
889
|
// Superseded: the row exists and shows up in the list, but the pane that asked for it is
|
|
885
890
|
// gone, so pointing `activeId` at it would send the NEXT pane's writes here. The caller's
|
|
@@ -964,4 +969,13 @@ export function createAssistantChatsClient({ ports }, _options = {}) {
|
|
|
964
969
|
useAssistantChats: ({ port }, options = {}) => useAssistantChats(port, options),
|
|
965
970
|
};
|
|
966
971
|
}
|
|
972
|
+
function savedMessageWriteKey(message) {
|
|
973
|
+
return messageWriteKey({ message }, {});
|
|
974
|
+
}
|
|
975
|
+
function renamedConversations({ current, id, title }) {
|
|
976
|
+
return current.map(conversation => conversation.id === id ? { ...conversation, title, titleSource: "manual" } : conversation);
|
|
977
|
+
}
|
|
978
|
+
function prependConversation(conversation) {
|
|
979
|
+
return (current) => [conversation, ...current];
|
|
980
|
+
}
|
|
967
981
|
//# sourceMappingURL=assistant-chats.js.map
|
|
@@ -32,8 +32,9 @@ function collectCompleted(head, { registry }) {
|
|
|
32
32
|
for (const ev of parseArtifacts({ content: head })) {
|
|
33
33
|
if (ev.type === 'artifact:start') {
|
|
34
34
|
meta = { identifier: ev.identifier, artifactType: ev.artifactType, title: ev.title };
|
|
35
|
+
continue;
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
if (ev.type === 'artifact:end') {
|
|
37
38
|
items.push(toItem({ ...meta, content: ev.fullContent, status: 'complete' }, { registry }));
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -186,12 +186,12 @@ export function useComposer(options = {}) {
|
|
|
186
186
|
const validate = validateAttachmentsRef.current;
|
|
187
187
|
if (!validate || !conversationId) {
|
|
188
188
|
attachmentsHydratedForRef.current = conversationId;
|
|
189
|
-
return;
|
|
189
|
+
return undefined;
|
|
190
190
|
}
|
|
191
191
|
const cached = readCachedAttachments({ conversationId: conversationId });
|
|
192
192
|
if (cached === null || cached.length === 0) {
|
|
193
193
|
attachmentsHydratedForRef.current = conversationId;
|
|
194
|
-
return;
|
|
194
|
+
return undefined;
|
|
195
195
|
}
|
|
196
196
|
let cancelled = false;
|
|
197
197
|
void (async () => {
|
|
@@ -19,12 +19,12 @@ function visualLineBounds({ textarea }) {
|
|
|
19
19
|
mirror.append(text, document.createTextNode('\u200b'));
|
|
20
20
|
document.body.append(mirror);
|
|
21
21
|
try {
|
|
22
|
-
|
|
22
|
+
const lineAt = (offset) => {
|
|
23
23
|
const range = document.createRange();
|
|
24
24
|
range.setStart(text, offset);
|
|
25
25
|
range.setEnd(text, offset);
|
|
26
26
|
return range.getBoundingClientRect().top;
|
|
27
|
-
}
|
|
27
|
+
};
|
|
28
28
|
const first = lineAt(0);
|
|
29
29
|
const last = lineAt(text.length);
|
|
30
30
|
// Both selection endpoints must be on the boundary visual line. Selecting across soft
|
|
@@ -52,7 +52,7 @@ export function useConversation(options) {
|
|
|
52
52
|
useEffect(() => {
|
|
53
53
|
const source = options.refreshSignal;
|
|
54
54
|
if (!source || run.isStreaming)
|
|
55
|
-
return;
|
|
55
|
+
return undefined;
|
|
56
56
|
let disposed = false, pending = false, timer;
|
|
57
57
|
const refresh = async () => {
|
|
58
58
|
if (pending)
|
|
@@ -61,7 +61,7 @@ export function useConversation(options) {
|
|
|
61
61
|
try {
|
|
62
62
|
const saved = await source.loadMessages();
|
|
63
63
|
if (!disposed)
|
|
64
|
-
setMessagesState(prev =>
|
|
64
|
+
setMessagesState(prev => mergeSavedMessages({ previous: prev, saved }));
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
67
|
if (!disposed)
|
|
@@ -143,14 +143,14 @@ export function useConversation(options) {
|
|
|
143
143
|
return m;
|
|
144
144
|
// A plain if/else chain (not a nested ternary) — deliberately, so
|
|
145
145
|
// each of the four mapped statuses is its own independently
|
|
146
|
-
// trackable branch under coverage instrumentation. The
|
|
146
|
+
// trackable branch under coverage instrumentation. The fallback
|
|
147
147
|
// value is asserted non-null: this reconciliation effect only ever
|
|
148
148
|
// runs after `sendMessage`/`retry` has called `run.start()`/
|
|
149
149
|
// `reattach` (both set the message's `runStatus: 'queued'` via
|
|
150
150
|
// `activeAssistantIdRef` first), so `m.runStatus` is always defined
|
|
151
151
|
// and `run.status` is never `'idle'` by the time this callback
|
|
152
152
|
// fires — one of the four branches below always reassigns it.
|
|
153
|
-
let runStatus
|
|
153
|
+
let runStatus;
|
|
154
154
|
if (run.status === 'streaming')
|
|
155
155
|
runStatus = 'running';
|
|
156
156
|
else if (run.status === 'done')
|
|
@@ -159,6 +159,8 @@ export function useConversation(options) {
|
|
|
159
159
|
runStatus = 'failed';
|
|
160
160
|
else if (run.status === 'canceled')
|
|
161
161
|
runStatus = 'canceled';
|
|
162
|
+
else
|
|
163
|
+
runStatus = m.runStatus;
|
|
162
164
|
const nextRunId = run.runId ?? m.runId;
|
|
163
165
|
const events = projectAcceptedInputEvents({ events: mergeRunEvents({ saved: m.events, incoming: run.events }, {}) });
|
|
164
166
|
return {
|
|
@@ -274,4 +276,7 @@ export function useConversation(options) {
|
|
|
274
276
|
suppressScroll,
|
|
275
277
|
}), [messages, conversationId, run.isStreaming, run.error, sendRefused, scrollIntent, sendMessage, cancel, retry, setMessages, acknowledgeScroll, suppressScroll]);
|
|
276
278
|
}
|
|
279
|
+
function mergeSavedMessages({ previous, saved }) {
|
|
280
|
+
return [...new Map([...previous, ...saved].map(message => [message.id, message])).values()];
|
|
281
|
+
}
|
|
277
282
|
//# sourceMappingURL=useConversation.js.map
|
|
@@ -26,12 +26,14 @@ function addExtEvent(state, { name, data }) {
|
|
|
26
26
|
function visitEvent(state, ev) {
|
|
27
27
|
if (ev.kind === 'tool_use') {
|
|
28
28
|
state.openCalls.set(ev.id, { name: ev.name, input: ev.input });
|
|
29
|
+
return;
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
if (ev.kind === 'tool_result') {
|
|
31
32
|
state.openCalls.delete(ev.toolUseId);
|
|
32
33
|
state.results.set(ev.toolUseId, { content: ev.content, isError: ev.isError });
|
|
34
|
+
return;
|
|
33
35
|
}
|
|
34
|
-
|
|
36
|
+
if (ev.kind === 'ext') {
|
|
35
37
|
addExtEvent(state, ev);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -48,7 +48,8 @@ export function useLatestOperation(_requiredArgs) {
|
|
|
48
48
|
generationRef.current += 1;
|
|
49
49
|
}, []);
|
|
50
50
|
const run = useCallback(async ({ body, onError }) => {
|
|
51
|
-
|
|
51
|
+
generationRef.current += 1;
|
|
52
|
+
const generation = generationRef.current;
|
|
52
53
|
const isCurrent = () => mountedRef.current && generation === generationRef.current;
|
|
53
54
|
const token = {
|
|
54
55
|
isCurrent,
|
|
@@ -66,23 +66,17 @@ export function parseSubmittedAnswers({ form, userMessageContent }) {
|
|
|
66
66
|
// `i < lines.length` is the loop condition, so this index is always
|
|
67
67
|
// in-bounds and defined — same reasoning as `header` above.
|
|
68
68
|
const line = lines[i];
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
69
|
+
const parsed = submittedAnswerLine({ line });
|
|
70
|
+
if (!parsed)
|
|
71
71
|
continue;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// `id` only ever comes from `labelToId`, which was populated from this
|
|
81
|
-
// same `form.questions` array immediately above, so a question with
|
|
82
|
-
// this exact `id` is always found — the `!` reflects that invariant
|
|
83
|
-
// rather than gaming an untestable defensive branch.
|
|
84
|
-
const question = form.questions.find((x) => x.id === id);
|
|
85
|
-
answers[id] = decodeAnswerValue(question, value);
|
|
72
|
+
const id = labelToId.get(parsed.labelKey);
|
|
73
|
+
if (id) {
|
|
74
|
+
// `id` only ever comes from `labelToId`, populated from this same `form.questions` array,
|
|
75
|
+
// so this exact question exists; the assertion reflects that invariant rather than adding
|
|
76
|
+
// an untestable defensive branch.
|
|
77
|
+
const question = form.questions.find((x) => x.id === id);
|
|
78
|
+
answers[id] = decodeAnswerValue(question, parsed.value);
|
|
79
|
+
}
|
|
86
80
|
}
|
|
87
81
|
return Object.keys(answers).length > 0 ? answers : null;
|
|
88
82
|
}
|
|
@@ -97,12 +91,28 @@ function decodeAnswerValue(question, value) {
|
|
|
97
91
|
return value.toLowerCase() === '(skipped)' ? '' : formOptionValueForLabel({ question: question, labelOrValue: parseSubmittedOptionToken(value) });
|
|
98
92
|
}
|
|
99
93
|
function parseSubmittedOptionToken(raw) {
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
94
|
+
const trimmed = raw.trimEnd();
|
|
95
|
+
if (!trimmed.endsWith(']'))
|
|
96
|
+
return raw.trim();
|
|
97
|
+
const end = trimmed.length - 1;
|
|
98
|
+
// The value cannot cross an earlier ']'; search the remaining suffix once, from the start
|
|
99
|
+
// of each whitespace run, retaining the first eligible (possibly nested) value marker.
|
|
100
|
+
const base = trimmed.lastIndexOf(']', end - 1) + 1;
|
|
101
|
+
const marker = /(?<!\s)\s+\[value:/i.exec(trimmed.slice(base, end));
|
|
102
|
+
if (!marker)
|
|
102
103
|
return raw.trim();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const value = trimmed.slice(base + marker.index + marker[0].length, end);
|
|
105
|
+
return value.length > 0 ? value.trim() : raw.trim();
|
|
106
|
+
}
|
|
107
|
+
function submittedAnswerLine({ line }) {
|
|
108
|
+
const label = /^[-*]([^:]+):/.exec(line);
|
|
109
|
+
if (!label)
|
|
110
|
+
return null;
|
|
111
|
+
const value = /^(.*)$/.exec(line.slice(label[0].length).trimStart());
|
|
112
|
+
if (!value)
|
|
113
|
+
return null;
|
|
114
|
+
// Each capture is required in its own pattern and is defined after a successful match;
|
|
115
|
+
// TypeScript cannot express that invariant for RegExpExecArray's numeric capture slots.
|
|
116
|
+
return { labelKey: label[1].trim().toLowerCase(), value: value[1].trim() };
|
|
107
117
|
}
|
|
108
118
|
//# sourceMappingURL=useQuestionForms.js.map
|
|
@@ -118,7 +118,8 @@ export function useRunStream({ transport }) {
|
|
|
118
118
|
}, []);
|
|
119
119
|
const start = useCallback(async (input) => {
|
|
120
120
|
teardownSubscription();
|
|
121
|
-
|
|
121
|
+
generationRef.current += 1;
|
|
122
|
+
const generation = generationRef.current;
|
|
122
123
|
runIdRef.current = null;
|
|
123
124
|
const subscriptionAbort = new AbortController();
|
|
124
125
|
const cancelAbort = new AbortController();
|
|
@@ -150,7 +151,8 @@ export function useRunStream({ transport }) {
|
|
|
150
151
|
}, [makeHandlers, teardownSubscription, transport]);
|
|
151
152
|
const reattach = useCallback(async (runId, initialEvents = []) => {
|
|
152
153
|
teardownSubscription();
|
|
153
|
-
|
|
154
|
+
generationRef.current += 1;
|
|
155
|
+
const generation = generationRef.current;
|
|
154
156
|
runIdRef.current = runId;
|
|
155
157
|
const subscriptionAbort = new AbortController();
|
|
156
158
|
const cancelAbort = new AbortController();
|
|
@@ -7,5 +7,6 @@ export declare function useUserTextGuard(options: UserTextRedactionOptions, _opt
|
|
|
7
7
|
}, _options?: {}) => import("../index.js").UserTextRedaction;
|
|
8
8
|
redactText: (text: string) => string;
|
|
9
9
|
notifyRedaction: (signal: SecretRedactionSignal) => void;
|
|
10
|
+
clearNotice: () => void;
|
|
10
11
|
};
|
|
11
12
|
//# sourceMappingURL=useUserTextGuard.d.ts.map
|
|
@@ -14,6 +14,8 @@ export function useUserTextGuard(options, _optional = {}) {
|
|
|
14
14
|
return result;
|
|
15
15
|
}, [options.redactUserText, notifyRedaction]);
|
|
16
16
|
const redactText = useCallback((text) => redact({ text }, {}).text, [redact]);
|
|
17
|
-
|
|
17
|
+
/** The notice is about one message: the pane clears it when the person sends the next one (a key in that one sets it again) or dismisses it. */
|
|
18
|
+
const clearNotice = useCallback(() => setSecretRedacted(false), []);
|
|
19
|
+
return { secretRedacted, redact, redactText, notifyRedaction, clearNotice };
|
|
18
20
|
}
|
|
19
21
|
//# sourceMappingURL=useUserTextGuard.js.map
|
|
@@ -100,9 +100,11 @@ function placeEvent({ state, ev }, { slotOf, includeThinking }) {
|
|
|
100
100
|
// Emitted per boundary, before any slot dedupe below, so every reply gets its own end marker.
|
|
101
101
|
if (isReplyBoundary({ event: ev }))
|
|
102
102
|
placeReplyEnd({ state });
|
|
103
|
-
if (ev.kind === 'ext')
|
|
103
|
+
if (ev.kind === 'ext') {
|
|
104
104
|
placeExt({ state, ev }, { slotOf });
|
|
105
|
-
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (ev.kind === 'tool_use')
|
|
106
108
|
placeToolUse({ state, id: ev.id });
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
@@ -43,6 +43,15 @@
|
|
|
43
43
|
min-width: 0;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* The picker, plus menu and send share this <div>; flex keeps them on one line, and it wraps
|
|
47
|
+
whole to its own line when the footer runs out of room. */
|
|
48
|
+
.jini-composer-footer-trailing {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
margin-left: auto;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
/* The grouped add menu and slash autocomplete anchor to the composer rather than escaping the
|
|
47
56
|
host's layout. Visual tokens remain the host's responsibility. */
|
|
48
57
|
.jini-composer,
|
|
@@ -32,6 +32,17 @@ export interface ToolDisplayRow {
|
|
|
32
32
|
export declare function isFailedToolRow({ row }: {
|
|
33
33
|
row: Pick<ToolDisplayRow, 'status'>;
|
|
34
34
|
}): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Ids of failed rows that a LATER successful call of the same tool and target retried (owner, 2026-10-31: a
|
|
37
|
+
* red "HTTP 400 … page was NOT written" row and "· 2 failed" stayed on screen after the immediate
|
|
38
|
+
* retry wrote the page). Those rows render as a neutral "Retried" and are not counted as failed;
|
|
39
|
+
* a failure nothing later succeeded at stays red. Inputs without target coordinates match by
|
|
40
|
+
* tool alone; a targetless success cannot clear a failure naming a target. Pass one turn's rows.
|
|
41
|
+
* @complexity O(n + target bytes) time and space.
|
|
42
|
+
*/
|
|
43
|
+
export declare function retriedToolRowIds({ rows }: {
|
|
44
|
+
rows: readonly ToolDisplayRow[];
|
|
45
|
+
}, _options?: {}): ReadonlySet<string>;
|
|
35
46
|
/**
|
|
36
47
|
* Applies a display mode to an interleaved message.
|
|
37
48
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { humanizeToolName } from '../core/index.js';
|
|
1
|
+
import { humanizeToolName, isDelegatedWrapperToolName } from '../core/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Whether a row failed: the tool returned an error, or the run
|
|
4
4
|
* ended without the tool ever answering (`deriveToolStatus` maps both to `'error'`).
|
|
@@ -7,6 +7,62 @@ import { humanizeToolName } from '../core/index.js';
|
|
|
7
7
|
export function isFailedToolRow({ row }) {
|
|
8
8
|
return row.status === 'error';
|
|
9
9
|
}
|
|
10
|
+
/** The tool a row called: a delegated wrapper's `input.toolId`, else its own name (wrappers share one name). */
|
|
11
|
+
function toolIdentity(row) {
|
|
12
|
+
if (!isDelegatedWrapperToolName({ name: row.name }))
|
|
13
|
+
return row.name;
|
|
14
|
+
const input = row.use?.input;
|
|
15
|
+
const toolId = typeof input === 'object' && input !== null ? input.toolId : undefined;
|
|
16
|
+
return typeof toolId === 'string' && toolId ? toolId : row.name;
|
|
17
|
+
}
|
|
18
|
+
/** Prefer a stable id over editable slugs/paths; normalize input aliases within each kind.
|
|
19
|
+
* @complexity O(target bytes), fixed field count; pure, absent coordinates return undefined.
|
|
20
|
+
*/
|
|
21
|
+
function targetCoordinate({ input }, _options = {}) {
|
|
22
|
+
const kinds = [
|
|
23
|
+
['id', ['postId', 'post_id', 'pageId', 'page_id', 'entryId', 'entry_id', 'id']],
|
|
24
|
+
['slug', ['slug']],
|
|
25
|
+
['path', ['path', 'filePath', 'file_path']],
|
|
26
|
+
];
|
|
27
|
+
for (const [kind, fields] of kinds) {
|
|
28
|
+
const field = fields.find(key => typeof input[key] === 'number' || (typeof input[key] === 'string' && input[key] !== ''));
|
|
29
|
+
if (field !== undefined)
|
|
30
|
+
return [kind, input[field]];
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Target coordinates only: corrected content must not prevent a retry from matching.
|
|
35
|
+
* Wrapper inputs belong to the delegated tool, whose identity toolIdentity already resolves.
|
|
36
|
+
* @complexity O(target bytes); the coordinate list is fixed. Pure, with no input mutation.
|
|
37
|
+
*/
|
|
38
|
+
function retryIdentity({ row }, _options = {}) {
|
|
39
|
+
const input = row.use?.input;
|
|
40
|
+
const record = typeof input === 'object' && input !== null ? input : {};
|
|
41
|
+
const target = isDelegatedWrapperToolName({ name: row.name }) ? record.input : record;
|
|
42
|
+
const coordinates = typeof target === 'object' && target !== null ? target : {};
|
|
43
|
+
return JSON.stringify([toolIdentity(row), targetCoordinate({ input: coordinates })]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ids of failed rows that a LATER successful call of the same tool and target retried (owner, 2026-10-31: a
|
|
47
|
+
* red "HTTP 400 … page was NOT written" row and "· 2 failed" stayed on screen after the immediate
|
|
48
|
+
* retry wrote the page). Those rows render as a neutral "Retried" and are not counted as failed;
|
|
49
|
+
* a failure nothing later succeeded at stays red. Inputs without target coordinates match by
|
|
50
|
+
* tool alone; a targetless success cannot clear a failure naming a target. Pass one turn's rows.
|
|
51
|
+
* @complexity O(n + target bytes) time and space.
|
|
52
|
+
*/
|
|
53
|
+
export function retriedToolRowIds({ rows }, _options = {}) {
|
|
54
|
+
const succeededLater = new Set();
|
|
55
|
+
const retried = new Set();
|
|
56
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
57
|
+
const row = rows[i];
|
|
58
|
+
const tool = retryIdentity({ row });
|
|
59
|
+
if (row.status === 'complete')
|
|
60
|
+
succeededLater.add(tool);
|
|
61
|
+
else if (isFailedToolRow({ row }) && succeededLater.has(tool))
|
|
62
|
+
retried.add(row.id);
|
|
63
|
+
}
|
|
64
|
+
return retried;
|
|
65
|
+
}
|
|
10
66
|
/** Running = no result yet while the run streams (`'executing'`), or not started (`'inProgress'`). */
|
|
11
67
|
function isRunningToolRow(row) {
|
|
12
68
|
return row.status === 'executing' || row.status === 'inProgress';
|
|
@@ -109,7 +165,8 @@ export function summarizeToolGroup({ rows }) {
|
|
|
109
165
|
if (isRunningToolRow(row))
|
|
110
166
|
return { kind: 'running', name: humanizeToolName({ name: row.name }, { input: row.use?.input }) };
|
|
111
167
|
}
|
|
112
|
-
|
|
168
|
+
const retried = retriedToolRowIds({ rows });
|
|
169
|
+
return { kind: 'done', count: rows.length, failed: rows.filter((row) => isFailedToolRow({ row }) && !retried.has(row.id)).length };
|
|
113
170
|
}
|
|
114
171
|
/**
|
|
115
172
|
* The collapsed line's text. Each phrase is its own translation key (the English string is the key,
|