@teaflask/assistant 0.8.0 → 0.9.0

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 (54) hide show
  1. package/dist/components/assistant-transcript.js +1 -7
  2. package/dist/components/attachment-chips.js +11 -5
  3. package/dist/components/composer.js +5 -6
  4. package/dist/components/message-list.js +2 -1
  5. package/dist/components/open-minted-url.js +12 -0
  6. package/dist/components/primitives/button.js +3 -0
  7. package/dist/components/tool-call-rows.js +1 -8
  8. package/dist/components/tool-row.js +1 -2
  9. package/dist/components/tool-view-slot.js +0 -1
  10. package/dist/components/tool-views/view-dom.d.ts +3 -4
  11. package/dist/components/tool-views/view-dom.js +1 -4
  12. package/dist/components/transcript-follow.js +12 -0
  13. package/dist/components/use-child-replay-stream.js +9 -2
  14. package/dist/components/use-transcript.d.ts +3 -2
  15. package/dist/components/user-message.js +26 -1
  16. package/dist/contract/events.d.ts +1 -0
  17. package/dist/contract/events.js +1 -0
  18. package/dist/contract/threads.d.ts +0 -5
  19. package/dist/contract/threads.js +0 -1
  20. package/dist/core/composer-policy.d.ts +1 -2
  21. package/dist/core/composer-policy.js +0 -2
  22. package/dist/core/connection-epoch.d.ts +1 -5
  23. package/dist/core/connection-epoch.js +23 -11
  24. package/dist/core/conversation-store.js +0 -1
  25. package/dist/core/epoch-sync.js +1 -1
  26. package/dist/core/error-copy.js +9 -1
  27. package/dist/core/long-message.js +22 -0
  28. package/dist/core/message-copy.js +1 -0
  29. package/dist/core/tool-call-display.d.ts +1 -7
  30. package/dist/core/tool-call-presentation.js +0 -3
  31. package/dist/core/tool-outcome.js +0 -1
  32. package/dist/core/tool-view.d.ts +2 -7
  33. package/dist/core/transcript-rows.d.ts +0 -6
  34. package/dist/core/transcript-rows.js +0 -3
  35. package/dist/core/withdrawn-anchors.d.ts +11 -0
  36. package/dist/core/withdrawn-anchors.js +76 -0
  37. package/dist/generated/models/apiErrorCode.d.ts +3 -0
  38. package/dist/generated/models/apiErrorCode.js +3 -0
  39. package/dist/generated/models/assistantStreamMarkerName.d.ts +1 -0
  40. package/dist/generated/models/assistantStreamMarkerName.js +1 -0
  41. package/dist/generated/models/index.d.ts +1 -0
  42. package/dist/generated/models/index.js +1 -0
  43. package/dist/generated/models/replyWithdrawnMarker.d.ts +4 -0
  44. package/dist/generated/models/replyWithdrawnMarker.js +1 -0
  45. package/dist/generated/models/servingErrorCode.d.ts +5 -4
  46. package/dist/generated/models/servingErrorCode.js +1 -0
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js +0 -1
  49. package/dist/styles.css +1 -1
  50. package/dist/transport/replay-stream-agent.js +4 -0
  51. package/dist/transport/stream-resume.d.ts +4 -2
  52. package/dist/transport/stream-resume.js +4 -4
  53. package/package.json +1 -1
  54. package/dist/core/tool-offload-anchors.js +0 -7
@@ -6,7 +6,6 @@ import { connectionErrorRecorder, messagesSnapshotRecorder, } from "../core/mess
6
6
  import { EMPTY_TOOL_CALL_DISPLAY_ANCHORS, toolCallDisplayRecorder, withToolCallDisplayAnchored, } from "../core/tool-call-display-anchors.js";
7
7
  import { EMPTY_TOOL_CANCEL_ANCHORS, toolCancelRecorder, withToolCancelAnchored, } from "../core/tool-cancel-anchors.js";
8
8
  import { EMPTY_TOOL_ERROR_ANCHORS, toolErrorRecorder, withToolErrorAnchored, } from "../core/tool-error-anchors.js";
9
- import { EMPTY_TOOL_OFFLOAD_ANCHORS, toolOffloadRecorder, withToolOffloadAnchored, } from "../core/tool-offload-anchors.js";
10
9
  import { EMPTY_TOOL_DENIAL_LEDGER, toolDenialRecorder, withToolDenialMarkerFolded, } from "../core/tool-denial-anchors.js";
11
10
  import { EMPTY_TOOL_REFUSAL_ANCHORS, toolRefusalRecorder, withToolRefusalAnchored, } from "../core/tool-refusal-anchors.js";
12
11
  import { transcriptBlocksOf, } from "../core/transcript-rows.js";
@@ -33,7 +32,6 @@ function useAgentRecording(agent, onStreamError) {
33
32
  const [displayAnchors, setDisplayAnchors] = useState(EMPTY_TOOL_CALL_DISPLAY_ANCHORS);
34
33
  const [errorAnchors, setErrorAnchors] = useState(EMPTY_TOOL_ERROR_ANCHORS);
35
34
  const [cancelAnchors, setCancelAnchors] = useState(EMPTY_TOOL_CANCEL_ANCHORS);
36
- const [offloadAnchors, setOffloadAnchors] = useState(EMPTY_TOOL_OFFLOAD_ANCHORS);
37
35
  const [refusalAnchors, setRefusalAnchors] = useState(EMPTY_TOOL_REFUSAL_ANCHORS);
38
36
  const [denialLedger, setDenialLedger] = useState(EMPTY_TOOL_DENIAL_LEDGER);
39
37
  const streamErrorRef = useLatestRef(onStreamError);
@@ -46,7 +44,6 @@ function useAgentRecording(agent, onStreamError) {
46
44
  agent.subscribe(toolCallDisplayRecorder(anchorInto(setDisplayAnchors, withToolCallDisplayAnchored))),
47
45
  agent.subscribe(toolErrorRecorder(anchorInto(setErrorAnchors, withToolErrorAnchored))),
48
46
  agent.subscribe(toolCancelRecorder(anchorInto(setCancelAnchors, withToolCancelAnchored))),
49
- agent.subscribe(toolOffloadRecorder(anchorInto(setOffloadAnchors, withToolOffloadAnchored))),
50
47
  agent.subscribe(toolRefusalRecorder(anchorInto(setRefusalAnchors, withToolRefusalAnchored))),
51
48
  agent.subscribe(toolDenialRecorder(anchorInto(setDenialLedger, withToolDenialMarkerFolded))),
52
49
  ];
@@ -64,13 +61,12 @@ function useAgentRecording(agent, onStreamError) {
64
61
  displayAnchors,
65
62
  errorAnchors,
66
63
  cancelAnchors,
67
- offloadAnchors,
68
64
  refusalAnchors,
69
65
  denialLedger,
70
66
  };
71
67
  }
72
68
  function AgentModeTranscript({ agent, onStreamError, markers, markerBoundaries, resumeAnchoredIds = NO_IDS, deliveryAnchoredIds = NO_IDS, turnAttachments, frameClaimsLiveness, scrollClassName, messageView, toolRow, input, welcomeScreen, threadList, }) {
73
- const { snapshot, displayAnchors, errorAnchors, cancelAnchors, offloadAnchors, refusalAnchors, denialLedger, } = useAgentRecording(agent, onStreamError);
69
+ const { snapshot, displayAnchors, errorAnchors, cancelAnchors, refusalAnchors, denialLedger, } = useAgentRecording(agent, onStreamError);
74
70
  const delivered = snapshot ?? EMPTY_SNAPSHOT;
75
71
  const anchors = useMemo(() => ({
76
72
  resumeAnchors: new Map([...resumeAnchoredIds].map((id) => [id, null])),
@@ -81,7 +77,6 @@ function AgentModeTranscript({ agent, onStreamError, markers, markerBoundaries,
81
77
  toolDenialAnchors: denialLedger.denied,
82
78
  toolCallDisplayAnchors: displayAnchors,
83
79
  turnFailedAnchors: NO_MAP,
84
- toolOffloadAnchors: offloadAnchors,
85
80
  toolSchemaAnchors: NO_MAP,
86
81
  blockTimingAnchors: NO_MAP,
87
82
  turnUsageAnchors: NO_MAP,
@@ -92,7 +87,6 @@ function AgentModeTranscript({ agent, onStreamError, markers, markerBoundaries,
92
87
  deliveryAnchoredIds,
93
88
  errorAnchors,
94
89
  cancelAnchors,
95
- offloadAnchors,
96
90
  refusalAnchors,
97
91
  denialLedger.denied,
98
92
  displayAnchors,
@@ -3,24 +3,30 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
4
  import { humanFileSize } from "./human-file-size.js";
5
5
  import { FileIcon } from "./icons.js";
6
+ import { openMintedUrlInNewWindow } from "./open-minted-url.js";
7
+ import { TfButton } from "./primitives/button.js";
6
8
  import { useOptionalAssistantSession } from "./teaflask-assistant-provider.js";
7
9
  export function AttachmentChips({ attachments, }) {
8
10
  if (attachments.length === 0) {
9
11
  return null;
10
12
  }
11
- return (_jsx("div", { className: "tf:flex tf:flex-wrap tf:justify-end tf:gap-2", children: attachments.map((attachment) => attachment.kind === "image" ? (_jsx(AttachmentImage, { attachment: attachment }, attachment.id)) : (_jsx(FileChip, { attachment: attachment }, attachment.id))) }));
13
+ return (_jsx("div", { className: "tf:flex tf:flex-wrap tf:items-end tf:justify-end tf:gap-2", children: attachments.map((attachment) => attachment.kind === "image" ? (_jsx(AttachmentImage, { attachment: attachment }, attachment.id)) : (_jsx(FileChip, { attachment: attachment }, attachment.id))) }));
12
14
  }
13
15
  function AttachmentImage({ attachment }) {
14
16
  const url = useAttachmentUrl(attachment.id);
15
17
  if (url === null) {
16
18
  return _jsx(FileChip, { attachment: attachment });
17
19
  }
18
- return (_jsx("img", { src: url, alt: attachment.filename, loading: "lazy", className: "tf:max-h-48 tf:max-w-full tf:rounded-lg tf:ring-1 tf:ring-tf-border tf:ring-inset" }));
20
+ return (_jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", "aria-label": `Open ${attachment.filename}`, className: "tf:block tf:max-w-full tf:rounded-lg tf:focus-visible:outline-2 " +
21
+ "tf:focus-visible:outline-offset-2 tf:focus-visible:outline-tf-ring", children: _jsx("img", { src: url, alt: attachment.filename, loading: "lazy", className: "tf:block tf:max-h-48 tf:max-w-full tf:rounded-lg tf:ring-1 tf:ring-tf-border tf:ring-inset" }) }));
19
22
  }
20
23
  function FileChip({ attachment }) {
21
- return (_jsxs("span", { className: "tf:inline-flex tf:max-w-full tf:items-center tf:gap-1.5 tf:rounded-lg " +
22
- "tf:bg-tf-background tf:px-2.5 tf:py-1.5 tf:text-xs tf:text-tf-foreground " +
23
- "tf:ring-1 tf:ring-tf-border tf:ring-inset", title: attachment.filename, children: [_jsx("span", { className: "tf:text-tf-muted-foreground", children: _jsx(FileIcon, {}) }), _jsx("span", { className: "tf:min-w-0 tf:truncate", children: attachment.filename }), _jsx("span", { className: "tf:shrink-0 tf:text-tf-muted-foreground", children: humanFileSize(attachment.byte_size) })] }));
24
+ const store = useOptionalAssistantSession()?.store ?? null;
25
+ return (_jsxs(TfButton, { variant: "fileTile", "aria-label": `Open ${attachment.filename}`, title: attachment.filename, onClick: () => {
26
+ if (store !== null) {
27
+ openMintedUrlInNewWindow(() => store.mintAttachmentDownloadUrl(attachment.id));
28
+ }
29
+ }, children: [_jsx("span", { className: "tf:text-tf-muted-foreground", children: _jsx(FileIcon, {}) }), _jsx("span", { className: "tf:min-w-0 tf:truncate", children: attachment.filename }), _jsx("span", { className: "tf:shrink-0 tf:text-tf-muted-foreground", children: humanFileSize(attachment.byte_size) })] }));
24
30
  }
25
31
  const URL_REFRESH_FLOOR_MS = 30_000;
26
32
  function useAttachmentUrl(attachmentId) {
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useLayoutEffect, useRef, useState, } from "react";
4
- import { ASSISTANT_MESSAGE_MAX_CHARS, } from "../contract/threads.js";
5
4
  import { attachmentAcceptOf, ComposerAttachmentChip, } from "./composer-attachment-chip.js";
6
5
  import { ComposerBanner } from "./composer-banner.js";
7
6
  import { useConversation } from "./conversation-context.js";
@@ -13,7 +12,7 @@ import { ModelPickerShelf } from "./model-picker-chip.js";
13
12
  import { useOptionalAssistantSession } from "./teaflask-assistant-provider.js";
14
13
  import { NO_GATE } from "../core/connect-gate.js";
15
14
  import { attachmentRoomOf } from "../core/composer-attachments.js";
16
- import { attachmentDraftFailed, capSlotsSpentBy, CHARACTER_COUNT_VISIBLE_FROM, composerKeyHandlersOf, } from "../core/composer-policy.js";
15
+ import { attachmentDraftFailed, capSlotsSpentBy, composerKeyHandlersOf, } from "../core/composer-policy.js";
17
16
  import { ObservableCell } from "../core/observable-cell.js";
18
17
  import { useCell } from "./use-store-cell.js";
19
18
  const NO_POLICY = new ObservableCell(null);
@@ -172,16 +171,16 @@ function ComposerSlabAndShelf({ draft, setDraft, attachments, policy, gate, busy
172
171
  "tf:transition-[border-color,box-shadow] " +
173
172
  "tf:focus-within:border-tf-foreground/25", children: [attachments.length > 0 ? (_jsx("div", { className: "tf:flex tf:flex-wrap tf:gap-1.5 tf:px-1 tf:pt-1 tf:pb-2", children: attachments.map((item) => (_jsx(ComposerAttachmentChip, { item: item, onRemove: () => {
174
173
  attach.removeAttachment(item.localId);
175
- } }, item.localId))) })) : null, _jsx(TfTextarea, { ref: textareaRef, rows: 2, variant: "bare", "data-tf-autofocus": "", value: draft, maxLength: ASSISTANT_MESSAGE_MAX_CHARS, placeholder: "Ask anything", "aria-label": "Message", onChange: (event) => {
174
+ } }, item.localId))) })) : null, _jsx(TfTextarea, { ref: textareaRef, rows: 2, variant: "bare", "data-tf-autofocus": "", value: draft, placeholder: "Ask anything", "aria-label": "Message", onChange: (event) => {
176
175
  setDraft(event.target.value);
177
- }, onKeyDown: composerKeys, onFocus: onTextareaFocus, onBlur: onTextareaBlur }), _jsx(ComposerControlsRow, { draft: draft, attachments: attachments, policy: policy, busy: busy, pendingSend: pendingSend, stopping: stopping, stopTurn: stopTurn, stopOnEscape: stopOnEscape, send: send, attach: attach, textareaRef: textareaRef })] }), gate.kind === "none" ? _jsx(ModelPickerShelf, {}) : null] }));
176
+ }, onKeyDown: composerKeys, onFocus: onTextareaFocus, onBlur: onTextareaBlur }), _jsx(ComposerControlsRow, { attachments: attachments, policy: policy, busy: busy, pendingSend: pendingSend, stopping: stopping, stopTurn: stopTurn, stopOnEscape: stopOnEscape, send: send, attach: attach, textareaRef: textareaRef })] }), gate.kind === "none" ? _jsx(ModelPickerShelf, {}) : null] }));
178
177
  }
179
- function ComposerControlsRow({ draft, attachments, policy, busy, pendingSend, stopping, stopTurn, stopOnEscape, send, attach, textareaRef, }) {
178
+ function ComposerControlsRow({ attachments, policy, busy, pendingSend, stopping, stopTurn, stopOnEscape, send, attach, textareaRef, }) {
180
179
  const fileInputRef = useRef(null);
181
180
  return (_jsxs("div", { className: "tf:flex tf:items-center tf:gap-2 tf:pt-1", children: [policy !== null ? (_jsxs(_Fragment, { children: [_jsx(TfFileInput, { ref: fileInputRef, multiple: true, accept: attachmentAcceptOf(policy), onPickFiles: attach.addFiles }), _jsx(TfButton, { variant: "icon", onClick: () => {
182
181
  fileInputRef.current?.click();
183
182
  }, disabled: capSlotsSpentBy(attachments) >= attach.attachmentRoom ||
184
- send.sending, "aria-label": "Attach a file", children: _jsx(PlusIcon, {}) })] })) : null, _jsxs("div", { className: "tf:ml-auto tf:flex tf:shrink-0 tf:items-center tf:gap-2", children: [stopping ? (_jsx("span", { role: "status", className: "tf:text-xs tf:text-tf-muted-foreground", children: "Stopping\u2026" })) : null, draft.length >= CHARACTER_COUNT_VISIBLE_FROM ? (_jsxs("span", { className: "tf:text-xs tf:text-tf-muted-foreground", children: [draft.length, "/", ASSISTANT_MESSAGE_MAX_CHARS] })) : null, busy && !pendingSend ? (_jsx(TfButton, { variant: "stop", onClick: () => {
183
+ send.sending, "aria-label": "Attach a file", children: _jsx(PlusIcon, {}) })] })) : null, _jsxs("div", { className: "tf:ml-auto tf:flex tf:shrink-0 tf:items-center tf:gap-2", children: [stopping ? (_jsx("span", { role: "status", className: "tf:text-xs tf:text-tf-muted-foreground", children: "Stopping\u2026" })) : null, busy && !pendingSend ? (_jsx(TfButton, { variant: "stop", onClick: () => {
185
184
  void stopTurn();
186
185
  textareaRef.current?.focus();
187
186
  }, onKeyDown: stopOnEscape, "aria-disabled": stopping, "aria-label": "Stop generating", children: _jsx(StopIcon, {}) })) : (_jsx(TfButton, { variant: "send", onClick: () => {
@@ -11,6 +11,7 @@ import { TfButton } from "./primitives/button.js";
11
11
  import { ScrollFollowGuards } from "./scroll-follow-guards.js";
12
12
  import { HostSlotBoundary } from "./slot-boundary.js";
13
13
  import { ShimmerText, TypingIndicator } from "./streaming-states.js";
14
+ import { ReleaseFollowProvider } from "./transcript-follow.js";
14
15
  import { useMessageListDerivations, } from "./use-transcript-folds.js";
15
16
  export function MessageList({ cards, typing = false, live = false, turnOpen, identityToken = PRIMARY_AGENT_IDENTITY_TOKEN, decisionSurfacesInShelf = false, markers: Markers, markerBoundaries, turnStartIds, frameClaimsLiveness = false, emptyFallback, scrollClassName, messageView, toolRow, placementHolds = true, ...source }) {
16
17
  const { foldedBlocks, displayRows, currentTurnStart, blockStarts, unrowedAnsweredAsks, liveIndex, hostEmptySurfaceShown, livenessClaimant, } = useMessageListDerivations(source, {
@@ -24,7 +25,7 @@ export function MessageList({ cards, typing = false, live = false, turnOpen, ide
24
25
  emptyFallback,
25
26
  });
26
27
  const rowAt = (row, flatIndex) => (_jsx(Row, { row: row, cards: cards, turnLive: live && flatIndex === liveIndex, inCurrentTurn: placementHolds && flatIndex >= currentTurnStart, identityToken: identityToken, decisionSurfacesInShelf: decisionSurfacesInShelf, messageView: messageView, toolRow: toolRow }, row.key));
27
- return (_jsxs(StickToBottom, { "data-tf-message-list": "", className: "tf:relative tf:min-h-0 tf:flex-1", resize: "instant", initial: "instant", role: "log", children: [_jsxs(StickToBottom.Content, { scrollClassName: scrollClassName, className: "tf:mx-auto tf:flex tf:w-full tf:max-w-3xl tf:flex-col tf:px-4 tf:py-4", children: [hostEmptySurfaceShown ? emptyFallback : null, foldedBlocks !== null ? (_jsx(MarkedBlocks, { blocks: foldedBlocks, blockStarts: blockStarts, markers: Markers, rowAt: rowAt })) : (displayRows.map((row, index) => rowAt(row, index))), _jsx(TranscriptTail, { unrowedAnsweredAsks: unrowedAnsweredAsks, livenessClaimant: livenessClaimant })] }), _jsx(ScrollFollowGuards, {}), _jsx(ScrollToBottom, { live: live })] }));
28
+ return (_jsxs(StickToBottom, { "data-tf-message-list": "", className: "tf:relative tf:min-h-0 tf:flex-1", resize: "instant", initial: "instant", role: "log", children: [_jsx(ReleaseFollowProvider, { children: _jsxs(StickToBottom.Content, { scrollClassName: scrollClassName, className: "tf:mx-auto tf:flex tf:w-full tf:max-w-3xl tf:flex-col tf:px-4 tf:py-4", children: [hostEmptySurfaceShown ? emptyFallback : null, foldedBlocks !== null ? (_jsx(MarkedBlocks, { blocks: foldedBlocks, blockStarts: blockStarts, markers: Markers, rowAt: rowAt })) : (displayRows.map((row, index) => rowAt(row, index))), _jsx(TranscriptTail, { unrowedAnsweredAsks: unrowedAnsweredAsks, livenessClaimant: livenessClaimant })] }) }), _jsx(ScrollFollowGuards, {}), _jsx(ScrollToBottom, { live: live })] }));
28
29
  }
29
30
  function MarkedBlocks({ blocks, blockStarts, markers: Markers, rowAt, }) {
30
31
  return blocks.map((block, blockIndex) => (_jsxs(Fragment, { children: [Markers !== undefined ? (_jsx(HostSlotBoundary, { slot: "markers", fallback: null, children: _jsx(Markers, { message: { id: block.messageId }, position: "before" }) })) : null, block.rows.map((row, index) => rowAt(row, blockStarts[blockIndex] + index)), Markers !== undefined ? (_jsx(HostSlotBoundary, { slot: "markers", fallback: null, children: _jsx(Markers, { message: { id: block.messageId }, position: "after" }) })) : null] }, block.messageId)));
@@ -0,0 +1,12 @@
1
+ export function openMintedUrlInNewWindow(mint) {
2
+ const opened = window.open("", "_blank");
3
+ if (opened === null) {
4
+ return;
5
+ }
6
+ opened.opener = null;
7
+ mint().then((minted) => {
8
+ opened.location = minted.url;
9
+ }, () => {
10
+ opened.close();
11
+ });
12
+ }
@@ -65,6 +65,9 @@ const VARIANT_CLASSES = {
65
65
  "tf:bg-tf-transparent tf:px-3 tf:py-2.5 tf:text-left tf:text-tf-label tf:text-tf-foreground " +
66
66
  "tf:wrap-anywhere tf:hover:bg-tf-accent tf:hover:text-tf-accent-foreground " +
67
67
  "tf:disabled:opacity-50 tf:aria-checked:bg-tf-emphasis tf:aria-checked:text-tf-foreground",
68
+ fileTile: "tf:max-w-full tf:cursor-pointer tf:rounded-lg tf:border-0 tf:bg-tf-background tf:px-2.5 tf:py-1.5 " +
69
+ "tf:text-xs tf:text-tf-foreground tf:ring-1 tf:ring-tf-border tf:ring-inset " +
70
+ "tf:hover:bg-tf-accent tf:hover:text-tf-accent-foreground",
68
71
  };
69
72
  export const TfButton = forwardRef(function TfButton({ variant = "primary", className, type, ...props }, ref) {
70
73
  return (_jsx("button", { ref: ref, type: type ?? "button", ...(variant === "secondary" ? { "data-tf-secondary": "" } : {}), className: cx("tf:appearance-none", "tf:inline-flex tf:items-center tf:gap-1.5 tf:transition-colors", "tf:focus-visible:outline-2 tf:focus-visible:outline-offset-2 tf:focus-visible:outline-tf-ring", "tf:disabled:cursor-not-allowed", VARIANT_CLASSES[variant], className), ...props }));
@@ -60,11 +60,6 @@ function _mandatoryStateWordOf(state, awaitingInput) {
60
60
  return null;
61
61
  }
62
62
  function _toolViewOf(row) {
63
- const offloaded = row.offloaded &&
64
- row.errorText === undefined &&
65
- row.state !== "denied" &&
66
- row.state !== "cancelled" &&
67
- row.state !== "refused";
68
63
  return {
69
64
  toolName: row.toolName,
70
65
  state: row.state,
@@ -74,11 +69,9 @@ function _toolViewOf(row) {
74
69
  output: row.errorText !== undefined ||
75
70
  row.state === "denied" ||
76
71
  row.state === "cancelled" ||
77
- row.state === "refused" ||
78
- offloaded
72
+ row.state === "refused"
79
73
  ? undefined
80
74
  : row.result,
81
- offloaded: offloaded || undefined,
82
75
  errorText: row.errorText,
83
76
  ...(row.refusalText !== undefined ? { refusalText: row.refusalText } : {}),
84
77
  display: row.display,
@@ -11,7 +11,6 @@ import { humanFailureSentenceOf } from "./tool-views/view-dom.js";
11
11
  function heldOpenOf(view, toolView, decision) {
12
12
  const hostViewBearing = toolView.views.some((candidate) => candidate.rung <= 2);
13
13
  return (isCleanState(view.state) &&
14
- view.offloaded !== true &&
15
14
  (hostViewBearing || decision) &&
16
15
  toolView.call.truncated !== true);
17
16
  }
@@ -30,5 +29,5 @@ export function ToolRow({ view, awaitingInput = false, decisionBearing, inCurren
30
29
  return (_jsxs(Disclosure, { variant: "bare", open: heldRowOpen || undefined, ...(view.toolCallId !== undefined && view.toolCallId !== ""
31
30
  ? { rootDataAttributes: { "data-tf-tool-call-id": view.toolCallId } }
32
31
  : {}), className: "tf:w-full", summaryClassName: "tf:flex tf:w-fit tf:items-center tf:gap-3 tf:py-1.5 tf:text-tf-label tf:text-tf-muted-foreground tf:hover:text-tf-foreground", bodyClassName: "tf:flex tf:flex-col tf:gap-2 tf:pt-1 tf:pb-2 tf:ps-7", summary: _jsxs(_Fragment, { children: [toolView.icons.length === 0 ? (_jsx(OperationMark, { icon: presentation.icon, status: presentation.status })) : (_jsx(ToolViewIconSlot, { icons: toolView.icons, call: toolView.call, icon: presentation.icon })), presentation.status === "input-available" ? (_jsx(ShimmerText, { className: "tf:leading-none", children: presentation.headline })) : (_jsx("span", { className: "tf:leading-none", children: presentation.headline })), presentation.status === "output-error" ? (_jsx(StatePill, { destructive: true, children: "Failed" })) : presentation.status === "denied" ? (_jsx(StatePill, { children: "Not approved" })) : presentation.status === "cancelled" ||
33
- presentation.status === "superseded" ? (_jsx(StatePill, { children: "Interrupted" })) : presentation.status === "refused" ? (_jsx(StatePill, { children: "Declined" })) : awaitingInput ? (_jsx(StatePill, { children: "Needs input" })) : null, _jsx(DisclosureChevron, { revealOnInteraction: true })] }), children: [_jsx("div", { "data-tf-semantic-view": "", className: "tf:mt-1", children: _jsx(ToolViewSlot, { views: toolView.views, call: toolView.call }) }), view.offloaded === true && (_jsx("p", { className: "tf:text-xs tf:text-tf-muted-foreground", children: "This result was too long to show in full, so the assistant worked from a shortened preview." })), view.refusalText !== undefined && (_jsx("p", { "data-tf-refusal-reason": "", className: "tf:text-xs tf:text-tf-muted-foreground", children: view.refusalText })), view.errorText !== undefined && (_jsx("p", { "data-tf-error-reason": "", className: "tf:text-xs tf:text-tf-muted-foreground", children: humanFailureSentenceOf(view.errorText) }))] }));
32
+ presentation.status === "superseded" ? (_jsx(StatePill, { children: "Interrupted" })) : presentation.status === "refused" ? (_jsx(StatePill, { children: "Declined" })) : awaitingInput ? (_jsx(StatePill, { children: "Needs input" })) : null, _jsx(DisclosureChevron, { revealOnInteraction: true })] }), children: [_jsx("div", { "data-tf-semantic-view": "", className: "tf:mt-1", children: _jsx(ToolViewSlot, { views: toolView.views, call: toolView.call }) }), view.refusalText !== undefined && (_jsx("p", { "data-tf-refusal-reason": "", className: "tf:text-xs tf:text-tf-muted-foreground", children: view.refusalText })), view.errorText !== undefined && (_jsx("p", { "data-tf-error-reason": "", className: "tf:text-xs tf:text-tf-muted-foreground", children: humanFailureSentenceOf(view.errorText) }))] }));
34
33
  }
@@ -142,7 +142,6 @@ function _sameDelivery(previous, next) {
142
142
  before.status === after.status &&
143
143
  before.awaitingDecision === after.awaitingDecision &&
144
144
  before.resultText === after.resultText &&
145
- before.offloaded === after.offloaded &&
146
145
  before.errorText === after.errorText &&
147
146
  before.refusalText === after.refusalText &&
148
147
  before.argsSchema === after.argsSchema &&
@@ -58,8 +58,7 @@ export declare function stringOf(value: unknown): string | null;
58
58
  export declare function statelessToolView(render: (props: ToolViewProps) => HTMLElement): ToolViewAdapter;
59
59
  /**
60
60
  * True when the call finished successfully and its own result is available
61
- * (status "output-available" and not offloaded). Check it before drawing a
62
- * result.
61
+ * (status "output-available"). Check it before drawing a result.
63
62
  */
64
63
  export declare function settledWithResult(call: ToolViewCall): boolean;
65
64
  /**
@@ -69,8 +68,8 @@ export declare function settledWithResult(call: ToolViewCall): boolean;
69
68
  export declare function settledOutcomeNotesOf(call: ToolViewCall): HTMLElement[];
70
69
  /**
71
70
  * A short sentence for a call that ended without a usable result (failed,
72
- * declined, not approved, didn't run, interrupted, or result replaced by a
73
- * preview). Null while the call is running or when it finished normally.
71
+ * declined, not approved, didn't run or interrupted). Null while the call is
72
+ * running or when it finished normally.
74
73
  */
75
74
  export declare function settledOutcomeNoteOf(call: ToolViewCall): string | null;
76
75
  /** An action's recorded HTTP response, as read by actionEnvelopeOf. */
@@ -140,7 +140,7 @@ export function statelessToolView(render) {
140
140
  };
141
141
  }
142
142
  export function settledWithResult(call) {
143
- return call.status === "output-available" && call.offloaded !== true;
143
+ return call.status === "output-available";
144
144
  }
145
145
  export function settledOutcomeNotesOf(call) {
146
146
  const sentence = settledOutcomeNoteOf(call);
@@ -163,9 +163,6 @@ export function settledOutcomeNoteOf(call) {
163
163
  case "output-available":
164
164
  break;
165
165
  }
166
- if (call.offloaded === true) {
167
- return "The result was replaced by a shortened preview and isn't rendered here.";
168
- }
169
166
  return null;
170
167
  }
171
168
  export function actionEnvelopeOf(resultText) {
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
4
+ import { useStickToBottomContext } from "use-stick-to-bottom";
5
+ const ReleaseFollowContext = createContext(() => undefined);
6
+ export function ReleaseFollowProvider({ children }) {
7
+ const { stopScroll } = useStickToBottomContext();
8
+ return (_jsx(ReleaseFollowContext.Provider, { value: stopScroll, children: children }));
9
+ }
10
+ export function useReleaseTranscriptFollow() {
11
+ return useContext(ReleaseFollowContext);
12
+ }
@@ -6,6 +6,7 @@ import { messagesRecorder, streamErrorRecorder, } from "../core/messages-cell.js
6
6
  import { TailSlotPool } from "../core/tail-slot-pool.js";
7
7
  import { toolCancelRecorder, withToolCancelAnchored, } from "../core/tool-cancel-anchors.js";
8
8
  import { toolErrorRecorder, withToolErrorAnchored, } from "../core/tool-error-anchors.js";
9
+ import { EMPTY_WITHDRAWN_REPLIES, withdrawnReplyRecorder, withoutWithdrawnReplies, withRepliesWithdrawn, } from "../core/withdrawn-anchors.js";
9
10
  import { ServingReplayStreamAgent } from "../transport/replay-stream-agent.js";
10
11
  import { streamUrlForChild } from "../transport/serving-api.js";
11
12
  import { resumeMarkerRecorder } from "../transport/stream-resume.js";
@@ -14,7 +15,6 @@ const THREADLESS_ANCHORS = {
14
15
  subagentDeliveryAnchors: new Map(),
15
16
  toolCallDisplayAnchors: new Map(),
16
17
  toolSchemaAnchors: new Map(),
17
- toolOffloadAnchors: new Set(),
18
18
  toolRefusalAnchors: new Map(),
19
19
  toolDecisionAnchors: new Set(),
20
20
  toolDenialAnchors: new Set(),
@@ -38,6 +38,7 @@ export function useChildReplayStream({ session, threadId, childSessionId, refres
38
38
  const [toolCancels, setToolCancels] = useState(() => new Set());
39
39
  const [resumeAnchors, setResumeAnchors] = useState(() => new Map());
40
40
  const [blockTimings, setBlockTimings] = useState(() => new Map());
41
+ const [withdrawnReplies, setWithdrawnReplies] = useState(EMPTY_WITHDRAWN_REPLIES);
41
42
  const { tailed, slotDenied } = useChildTailLease(streamEnd);
42
43
  useEffect(() => {
43
44
  if (!tailed) {
@@ -56,6 +57,7 @@ export function useChildReplayStream({ session, threadId, childSessionId, refres
56
57
  setToolCancels,
57
58
  setResumeAnchors,
58
59
  setBlockTimings,
60
+ setWithdrawnReplies,
59
61
  },
60
62
  });
61
63
  }, [session, threadId, childSessionId, tailed, attempt, refreshLedger]);
@@ -66,8 +68,9 @@ export function useChildReplayStream({ session, threadId, childSessionId, refres
66
68
  toolCancelAnchors: toolCancels,
67
69
  blockTimingAnchors: blockTimings,
68
70
  }), [resumeAnchors, toolErrors, toolCancels, blockTimings]);
71
+ const visibleMessages = useMemo(() => withoutWithdrawnReplies(messages, withdrawnReplies), [messages, withdrawnReplies]);
69
72
  return {
70
- messages,
73
+ messages: visibleMessages,
71
74
  streamEnd,
72
75
  tailed,
73
76
  slotDenied,
@@ -176,11 +179,15 @@ function subscribeAnchorTaps(agent, record) {
176
179
  : previous;
177
180
  });
178
181
  }));
182
+ const withdrawnTap = agent.subscribe(withdrawnReplyRecorder((marker) => {
183
+ record.setWithdrawnReplies((previous) => withRepliesWithdrawn(previous, marker));
184
+ }));
179
185
  return () => {
180
186
  messagesTap.unsubscribe();
181
187
  errorAnchorTap.unsubscribe();
182
188
  cancelAnchorTap.unsubscribe();
183
189
  resumeAnchorTap.unsubscribe();
184
190
  timingAnchorTap.unsubscribe();
191
+ withdrawnTap.unsubscribe();
185
192
  };
186
193
  }
@@ -38,7 +38,7 @@ export interface TranscriptSurface {
38
38
  */
39
39
  decisionsPending: boolean;
40
40
  /**
41
- * The raw message list behind the rows (user messages, assistant text,
41
+ * The message list behind the rows (user messages, assistant text,
42
42
  * reasoning, tool calls and tool results). Use it with markerAnchors,
43
43
  * running and turnMeta to call transcriptRowsOf yourself.
44
44
  */
@@ -65,7 +65,8 @@ export interface TranscriptSurface {
65
65
  * as rows plus run state. Call it inside TeaflaskAssistantProvider (it throws
66
66
  * otherwise); keep it mounted while the transcript is on screen, because that
67
67
  * is what makes the assistant connect and load an existing conversation's
68
- * history.
68
+ * history. A reply part the assistant withdrew is left out of messages and
69
+ * rows; the re-sent answer follows.
69
70
  */
70
71
  export declare function useTranscript(options?: {
71
72
  surface?: string;
@@ -1,9 +1,34 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useId, useRef, useState } from "react";
4
+ import { longMessagePreviewOf } from "../core/long-message.js";
3
5
  import { conversationalTimeOf } from "../core/time-labels.js";
4
6
  import { AttachmentChips } from "./attachment-chips.js";
5
7
  import { CopyTextButton } from "./copy-text-button.js";
8
+ import { TfButton } from "./primitives/button.js";
9
+ import { useReleaseTranscriptFollow } from "./transcript-follow.js";
6
10
  export function UserMessage({ children, attachments = [], createdAt, }) {
7
11
  const time = createdAt !== undefined ? conversationalTimeOf(createdAt) : null;
8
- return (_jsxs("div", { className: "tf:group/user-message tf:flex tf:flex-col tf:items-end tf:gap-2 tf:ps-20", "data-role": "user", children: [_jsx(AttachmentChips, { attachments: attachments }), children !== "" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "tf:min-w-0 tf:rounded-xl tf:bg-tf-muted tf:px-4 tf:py-2 tf:text-tf-body tf:break-words tf:whitespace-pre-wrap tf:text-tf-foreground", children: children }), _jsxs("div", { className: "tf:flex tf:h-7 tf:items-center tf:justify-end tf:gap-1", children: [time !== null ? (_jsx("time", { dateTime: createdAt, title: time.full, className: "tf:text-xs tf:text-tf-muted-foreground tf:opacity-0 tf:transition-opacity tf:group-hover/user-message:opacity-100 tf:group-focus-within/user-message:opacity-100 tf:pointer-coarse:opacity-100", children: time.label })) : null, _jsx(CopyTextButton, { text: children, label: "Copy message", className: "tf:opacity-0 tf:transition-opacity tf:group-hover/user-message:opacity-100 tf:group-focus-within/user-message:opacity-100 tf:focus-visible:opacity-100 tf:pointer-coarse:opacity-100" })] })] })) : null] }));
12
+ return (_jsxs("div", { className: "tf:group/user-message tf:flex tf:flex-col tf:items-end tf:gap-2 tf:ps-20", "data-role": "user", children: [_jsx(AttachmentChips, { attachments: attachments }), children !== "" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "tf:min-w-0 tf:rounded-xl tf:bg-tf-muted tf:px-4 tf:py-2 tf:text-tf-body tf:break-words tf:whitespace-pre-wrap tf:text-tf-foreground", children: _jsx(UserMessageText, { text: children }) }), _jsxs("div", { className: "tf:flex tf:h-7 tf:items-center tf:justify-end tf:gap-1", children: [time !== null ? (_jsx("time", { dateTime: createdAt, title: time.full, className: "tf:text-xs tf:text-tf-muted-foreground tf:opacity-0 tf:transition-opacity tf:group-hover/user-message:opacity-100 tf:group-focus-within/user-message:opacity-100 tf:pointer-coarse:opacity-100", children: time.label })) : null, _jsx(CopyTextButton, { text: children, label: "Copy message", className: "tf:opacity-0 tf:transition-opacity tf:group-hover/user-message:opacity-100 tf:group-focus-within/user-message:opacity-100 tf:focus-visible:opacity-100 tf:pointer-coarse:opacity-100" })] })] })) : null] }));
13
+ }
14
+ function UserMessageText({ text }) {
15
+ const preview = longMessagePreviewOf(text);
16
+ const [expanded, setExpanded] = useState(false);
17
+ const textId = useId();
18
+ const toggleRef = useRef(null);
19
+ const releaseFollow = useReleaseTranscriptFollow();
20
+ if (preview === null) {
21
+ return text;
22
+ }
23
+ return (_jsxs(_Fragment, { children: [_jsx("div", { id: textId, children: expanded ? text : `${preview}…` }), _jsx("div", { className: "tf:sticky tf:bottom-0 tf:bg-tf-muted tf:pt-1", children: _jsx(TfButton, { ref: toggleRef, variant: "bare", "aria-expanded": expanded, "aria-controls": textId, className: "tf:min-h-6 tf:text-tf-label tf:text-tf-muted-foreground tf:hover:text-tf-foreground", onClick: () => {
24
+ if (expanded) {
25
+ requestAnimationFrame(() => {
26
+ toggleRef.current?.scrollIntoView({ block: "nearest" });
27
+ });
28
+ }
29
+ else {
30
+ releaseFollow();
31
+ }
32
+ setExpanded(!expanded);
33
+ }, children: expanded ? "Show less" : "Show more" }) })] }));
9
34
  }
@@ -1,5 +1,6 @@
1
1
  import type { ApprovalRequestedMarker, ApprovalResolvedMarker, InterruptAnswerConsumedMarker, SubagentResultsDeliveredMarker, ToolExecutionRequestedMarker } from "../generated/models/index.js";
2
2
  export declare const RUN_RESUMED_EVENT_NAME: "run_resumed";
3
+ export declare const REPLY_WITHDRAWN_EVENT_NAME: "reply_withdrawn";
3
4
  export declare const APPROVAL_REQUESTED_EVENT_NAME: "approval_requested";
4
5
  export type ApprovalRequestedPayload = ApprovalRequestedMarker & Required<Pick<ApprovalRequestedMarker, "tool_args" | "tool_input_schema" | "tool_output_schema" | "round" | "gated" | "trust_available">>;
5
6
  export declare const APPROVAL_RESOLVED_EVENT_NAME: "approval_resolved";
@@ -1,5 +1,6 @@
1
1
  import { AssistantStreamMarkerName } from "../generated/models/index.js";
2
2
  export const RUN_RESUMED_EVENT_NAME = AssistantStreamMarkerName.run_resumed;
3
+ export const REPLY_WITHDRAWN_EVENT_NAME = AssistantStreamMarkerName.reply_withdrawn;
3
4
  export const APPROVAL_REQUESTED_EVENT_NAME = AssistantStreamMarkerName.approval_requested;
4
5
  export const APPROVAL_RESOLVED_EVENT_NAME = AssistantStreamMarkerName.approval_resolved;
5
6
  export const INTERRUPT_ANSWER_CONSUMED_EVENT_NAME = AssistantStreamMarkerName.interrupt_answer_consumed;
@@ -1,7 +1,2 @@
1
1
  export type { CreateServingAttachmentUploadResponse as AttachmentUploadIntent, HighlightResult, NavigateResult, PrefillFormResult, ReadPageResult, ResolveServingTurnApprovalRequest as ResolveTurnApprovalRequest, ResolveServingTurnApprovalResponse as ResolveTurnApprovalResponse, ResolveServingTurnToolResultsRequest as ResolveTurnToolResultsRequest, ResolveServingTurnToolResultsResponse as ResolveTurnToolResultsResponse, SendServingAssistantMessageRequest as SendAssistantMessageRequest, SendServingAssistantMessageResponse as SendAssistantMessageResponse, ServingAssistantThreadDetailResponse as AssistantThreadDetailResponse, ServingAssistantThreadResponse as ServingAssistantThread, ServingAssistantTurnResponse as ServingAssistantTurn, ServingAttachmentResponse as ServingAttachment, ServingTurnAttachmentResponse as TurnAttachment, StopServingTurnResponse as StopTurnResponse, ThreadModelPickResponse as ThreadModelPick, AssistantTurnStatus as TurnStatus, } from "../generated/models/index.js";
2
- /**
3
- * The longest message a user can send, in characters; the composer stops input
4
- * at this length and the server rejects longer messages.
5
- */
6
- export declare const ASSISTANT_MESSAGE_MAX_CHARS = 4000;
7
2
  export declare const ASSISTANT_MAX_ATTACHMENTS = 10;
@@ -1,2 +1 @@
1
- export const ASSISTANT_MESSAGE_MAX_CHARS = 4000;
2
1
  export const ASSISTANT_MAX_ATTACHMENTS = 10;
@@ -1,6 +1,5 @@
1
- import { type ServingAttachment } from "../contract/threads.js";
1
+ import type { ServingAttachment } from "../contract/threads.js";
2
2
  import type { AttachmentKind } from "../generated/models/index.js";
3
- export declare const CHARACTER_COUNT_VISIBLE_FROM: number;
4
3
  /** One file staged in the composer for the next message. */
5
4
  export interface AttachmentDraft<A = ServingAttachment> {
6
5
  /**
@@ -1,5 +1,3 @@
1
- import { ASSISTANT_MESSAGE_MAX_CHARS, } from "../contract/threads.js";
2
- export const CHARACTER_COUNT_VISIBLE_FROM = ASSISTANT_MESSAGE_MAX_CHARS - 400;
3
1
  let nextDraftId = 1;
4
2
  export function mintAttachmentDraftId() {
5
3
  return nextDraftId++;
@@ -49,11 +49,6 @@ export interface TranscriptMarkers {
49
49
  * The tool's explanation for each call it declined, keyed by tool call id.
50
50
  */
51
51
  toolRefusalAnchors: ReadonlyMap<string, string>;
52
- /**
53
- * Ids of tool calls whose output was too large and was replaced by a
54
- * shortened stand-in.
55
- */
56
- toolOffloadAnchors: ReadonlySet<string>;
57
52
  /**
58
53
  * Ids of tool calls the user was asked to approve or answer a question for.
59
54
  * Treat a missing set as empty.
@@ -123,6 +118,7 @@ export interface ConnectionEpoch {
123
118
  threadId: string;
124
119
  streamThreadId: string;
125
120
  withExecutionInbox: (mutate: (inbox: ExecutionInbox) => boolean) => void;
121
+ visibleMessages: () => readonly Message[];
126
122
  dispose: () => void;
127
123
  }
128
124
  export declare function beginConnectionEpoch(deps: ConnectionEpochDeps): ConnectionEpoch;
@@ -14,11 +14,11 @@ import { toolCancelRecorder, wellFormedToolCancelAnchorsOf, } from "./tool-cance
14
14
  import { toolErrorRecorder, wellFormedToolErrorAnchorsOf, } from "./tool-error-anchors.js";
15
15
  import { toolDecisionRecorder, wellFormedToolDecisionAnchorsOf, } from "./tool-decision-anchors.js";
16
16
  import { toolDenialRecorder, wellFormedToolDenialAnchorsOf, } from "./tool-denial-anchors.js";
17
- import { toolOffloadRecorder, wellFormedToolOffloadAnchorsOf, } from "./tool-offload-anchors.js";
18
17
  import { toolRefusalRecorder, wellFormedToolRefusalAnchorsOf, } from "./tool-refusal-anchors.js";
19
18
  import { turnFailedMarkerRecorder, wellFormedFailAnchorsOf, } from "./turn-failed-anchors.js";
20
19
  import { turnUsageRecorder, wellFormedTurnUsageAnchorsOf, } from "./turn-usage-anchors.js";
21
20
  import { messagesRecorder, streamErrorRecorder } from "./messages-cell.js";
21
+ import { withdrawnReplyRecorder, withoutWithdrawnReplies, } from "./withdrawn-anchors.js";
22
22
  import { userTurnInjector } from "./user-turn-ledger.js";
23
23
  export function markerAnchorsOf(resume) {
24
24
  return {
@@ -28,7 +28,6 @@ export function markerAnchorsOf(resume) {
28
28
  toolErrorAnchors: wellFormedToolErrorAnchorsOf(resume.toolErrorAnchors),
29
29
  toolCancelAnchors: wellFormedToolCancelAnchorsOf(resume.toolCancelAnchors),
30
30
  toolRefusalAnchors: wellFormedToolRefusalAnchorsOf(resume.toolRefusalAnchors),
31
- toolOffloadAnchors: wellFormedToolOffloadAnchorsOf(resume.toolOffloadAnchors),
32
31
  toolDecisionAnchors: wellFormedToolDecisionAnchorsOf(resume.toolDecisionAnchors),
33
32
  toolDenialAnchors: wellFormedToolDenialAnchorsOf(resume.toolDenialLedger.denied),
34
33
  toolCallDisplayAnchors: wellFormedToolCallDisplayAnchorsOf(resume.toolCallDisplayAnchors),
@@ -83,6 +82,7 @@ export function beginConnectionEpoch(deps) {
83
82
  publishEntries(executionInbox.entries());
84
83
  }
85
84
  },
85
+ visibleMessages: () => _visibleMessagesOf(agent, deps),
86
86
  dispose: () => {
87
87
  for (const subscription of subscriptions) {
88
88
  subscription.unsubscribe();
@@ -92,6 +92,9 @@ export function beginConnectionEpoch(deps) {
92
92
  },
93
93
  };
94
94
  }
95
+ function _visibleMessagesOf(agent, deps) {
96
+ return withoutWithdrawnReplies([...agent.messages], deps.resume.withdrawnReplies);
97
+ }
95
98
  function _runMarkerSubscriptionsOf(agent, deps) {
96
99
  return [
97
100
  agent.subscribe(resumeMarkerRecorder((runId, messageId, markerValue, markerTimestamp) => {
@@ -103,7 +106,7 @@ function _runMarkerSubscriptionsOf(agent, deps) {
103
106
  deps.noteInterruptAnswerConsumed(runId, consumed.interruptId, consumed.round);
104
107
  })),
105
108
  agent.subscribe(turnFailedMarkerRecorder((failedRunId, messageId, position, receipts) => {
106
- if (deps.resume.recordTurnFailedReceipts(failedRunId, messageId, position, receipts)) {
109
+ if (deps.resume.recordTurnFailedReceipts(failedRunId, _visibleAnchorOf(messageId, agent, deps), position, receipts)) {
107
110
  deps.publishMarkerAnchors();
108
111
  }
109
112
  })),
@@ -112,8 +115,22 @@ function _runMarkerSubscriptionsOf(agent, deps) {
112
115
  deps.publishMarkerAnchors();
113
116
  }
114
117
  })),
118
+ agent.subscribe(withdrawnReplyRecorder((marker) => {
119
+ const before = deps.resume.withdrawnReplies;
120
+ deps.resume.recordRepliesWithdrawn(marker);
121
+ if (deps.resume.withdrawnReplies !== before) {
122
+ deps.publishMessages(_visibleMessagesOf(agent, deps));
123
+ }
124
+ })),
115
125
  ];
116
126
  }
127
+ function _visibleAnchorOf(messageId, agent, deps) {
128
+ const visible = _visibleMessagesOf(agent, deps);
129
+ if (visible.some((message) => message.id === messageId)) {
130
+ return messageId;
131
+ }
132
+ return visible.at(-1)?.id ?? messageId;
133
+ }
117
134
  function _toolMarkerSubscriptionsOf(agent, deps) {
118
135
  return [
119
136
  agent.subscribe(toolErrorRecorder((toolCallId, errorText) => {
@@ -151,13 +168,6 @@ function _toolMarkerSubscriptionsOf(agent, deps) {
151
168
  deps.publishMarkerAnchors();
152
169
  }
153
170
  })),
154
- agent.subscribe(toolOffloadRecorder((toolCallId) => {
155
- const before = deps.resume.toolOffloadAnchors;
156
- deps.resume.recordToolOffload(toolCallId);
157
- if (deps.resume.toolOffloadAnchors !== before) {
158
- deps.publishMarkerAnchors();
159
- }
160
- })),
161
171
  agent.subscribe(toolCallDisplayRecorder((toolCallId, display) => {
162
172
  const before = deps.resume.toolCallDisplayAnchors;
163
173
  deps.resume.recordToolCallDisplay(toolCallId, display);
@@ -226,7 +236,9 @@ function _streamStateSubscriptionsOf(agent, deps, inboxes) {
226
236
  deps.onQuietClose(() => agent.isRunning);
227
237
  })),
228
238
  agent.subscribe(executionInboxRecorder(inboxes.executionInbox, inboxes.publishEntries)),
229
- agent.subscribe(messagesRecorder(deps.publishMessages)),
239
+ agent.subscribe(messagesRecorder((messages) => {
240
+ deps.publishMessages(withoutWithdrawnReplies(messages, deps.resume.withdrawnReplies));
241
+ })),
230
242
  agent.subscribe(streamErrorRecorder(deps.onStreamError)),
231
243
  ];
232
244
  }
@@ -132,7 +132,6 @@ export class AssistantConversationStore {
132
132
  toolErrorAnchors: new Map(),
133
133
  toolRefusalAnchors: new Map(),
134
134
  toolCancelAnchors: new Set(),
135
- toolOffloadAnchors: new Set(),
136
135
  toolDecisionAnchors: new Set(),
137
136
  toolDenialAnchors: new Set(),
138
137
  toolCallDisplayAnchors: new Map(),
@@ -38,7 +38,7 @@ export function syncEpoch(store) {
38
38
  store._questionAnswers.clear();
39
39
  publishExecutionEntries(store, []);
40
40
  store._epoch.withExecutionInbox((inbox) => inbox.adoptCoworkerRequests(active.coworkerRequestTurns));
41
- store._messagesCell.set([...store._epoch.agent.messages]);
41
+ store._messagesCell.set(store._epoch.visibleMessages());
42
42
  store._markerAnchorsCell.set(markerAnchorsOf(active.resume));
43
43
  store._connectionCell.set({
44
44
  epochId,