| 1 | import { restoreAttachmentRefsForSubmit } from "./attachmentDisplay"; |
| 2 | import { invocationSegmentsFromMessage, serializeInvocationSubmit, type ComposerInvocation } from "./invocationDisplay"; |
| 3 | import { splitSelectedTextContext } from "./selectedTextContext"; |
| 4 | |
| 5 | export function replaySubmitText( |
| 6 | originalSubmitText: string | undefined, |
| 7 | originalDisplayText: string, |
| 8 | nextDisplayText: string, |
| 9 | fallbackSubmitText: string, |
| 10 | ): string { |
| 11 | const originalSubmit = (originalSubmitText ?? "").trim(); |
| 12 | const originalDisplay = originalDisplayText.trim(); |
| 13 | const nextDisplay = nextDisplayText.trim(); |
| 14 | const fallbackSubmit = fallbackSubmitText.trim(); |
| 15 | if (!originalSubmit || originalSubmit === originalDisplay) return fallbackSubmit; |
| 16 | if (nextDisplay === originalDisplay) return originalSubmit; |
| 17 | |
| 18 | const invocationSegments = invocationSegmentsFromMessage(originalDisplay, originalSubmit); |
| 19 | const invocationItems = invocationSegments.filter((segment) => segment.type === "invocation"); |
| 20 | if (invocationItems.length > 0) { |
| 21 | const scale = originalDisplay.length > 0 ? nextDisplay.length / originalDisplay.length : 0; |
| 22 | const invocations: ComposerInvocation[] = invocationItems.map((segment, index) => ({ |
| 23 | id: `edit-invocation-${index}`, |
| 24 | offset: index === 0 ? 0 : Math.min(nextDisplay.length, Math.round(segment.offset * scale)), |
| 25 | command: { |
| 26 | name: segment.invocation.name, |
| 27 | description: "", |
| 28 | kind: segment.invocation.kind ?? "skill", |
| 29 | }, |
| 30 | })); |
| 31 | const serialized = serializeInvocationSubmit(fallbackSubmit, invocations).trim(); |
| 32 | // Keep any hidden submit prefix (referenced-session context, memory |
| 33 | // framing) exactly like the plain-text branch below: the display maps to |
| 34 | // the serialized slash tail of the original submit, and everything before |
| 35 | // that tail rides along unchanged. No match (e.g. attachment-expanded |
| 36 | // tails) falls back to the bare serialized form. |
| 37 | const originalInvocations: ComposerInvocation[] = invocationItems.map((segment, index) => ({ |
| 38 | id: `edit-invocation-original-${index}`, |
| 39 | offset: segment.offset, |
| 40 | command: { |
| 41 | name: segment.invocation.name, |
| 42 | description: "", |
| 43 | kind: segment.invocation.kind ?? "skill", |
| 44 | }, |
| 45 | })); |
| 46 | const originalSerialized = serializeInvocationSubmit(originalDisplay, originalInvocations).trim(); |
| 47 | if (originalSerialized && originalSubmit.length > originalSerialized.length && originalSubmit.endsWith(originalSerialized)) { |
| 48 | return `${originalSubmit.slice(0, originalSubmit.length - originalSerialized.length)}${serialized}`.trim(); |
| 49 | } |
| 50 | return serialized; |
| 51 | } |
| 52 | |
| 53 | const originalFallbackSubmit = restoreAttachmentRefsForSubmit(originalDisplay).trim(); |
| 54 | if (originalFallbackSubmit && originalSubmit.endsWith(originalFallbackSubmit)) { |
| 55 | return `${originalSubmit.slice(0, originalSubmit.length - originalFallbackSubmit.length)}${fallbackSubmit}`.trim(); |
| 56 | } |
| 57 | return fallbackSubmit; |
| 58 | } |
| 59 | |
| 60 | export function replaySubmitTextPreservingSelectedContext( |
| 61 | originalSubmitText: string | undefined, |
| 62 | originalDisplayText: string, |
| 63 | nextDisplayText: string, |
| 64 | fallbackSubmitText: string, |
| 65 | ): string { |
| 66 | const selected = splitSelectedTextContext(originalSubmitText); |
| 67 | const replayed = replaySubmitText( |
| 68 | selected.submitText, |
| 69 | originalDisplayText, |
| 70 | nextDisplayText, |
| 71 | fallbackSubmitText, |
| 72 | ); |
| 73 | return [replayed, selected.contextBlock].filter(Boolean).join("\n\n"); |
| 74 | } |
| 75 |