import { expandPastedTextRefs, formatPastedTextRef, getPastedTextRefNumLines, } from '../history.js' import instances from '../ink/instances.js' import type { PastedContent } from './config.js' import { classifyGuiEditor, getExternalEditor } from './editor.js' import { execSync_DEPRECATED } from './execSyncWrapper.js' import { getFsImplementation } from './fsOperations.js' import { toIDEDisplayName } from './ide.js' import { writeFileSync_DEPRECATED } from './slowOperations.js' import { generateTempFilePath } from './tempfile.js' // Map of editor command overrides (e.g., to add wait flags) const EDITOR_OVERRIDES: Record = { code: 'code -w', // VS Code: wait for file to be closed subl: 'subl --wait', // Sublime Text: wait for file to be closed } function isGuiEditor(editor: string): boolean { return classifyGuiEditor(editor) !== undefined } export type EditorResult = { content: string | null error?: string } // sync IO: called from sync context (React components, sync command handlers) export function editFileInEditor(filePath: string): EditorResult { const fs = getFsImplementation() const inkInstance = instances.get(process.stdout) if (!inkInstance) { throw new Error('Ink instance not found - cannot pause rendering') } const editor = getExternalEditor() if (!editor) { return { content: null } } try { fs.statSync(filePath) } catch { return { content: null } } const useAlternateScreen = !isGuiEditor(editor) if (useAlternateScreen) { // Terminal editors (vi, nano, etc.) take over the terminal. Delegate to // Ink's alt-screen-aware handoff so fullscreen mode (where // already entered alt screen) doesn't get knocked back to the main buffer // by a hardcoded ?1049l. enterAlternateScreen() internally calls pause() // and suspendStdin(); exitAlternateScreen() undoes both and resets frame // state so the next render writes from scratch. inkInstance.enterAlternateScreen() } else { // GUI editors (code, subl, etc.) open in a separate window — just pause // Ink and release stdin while they're open. inkInstance.pause() inkInstance.suspendStdin() } try { // Use override command if available, otherwise use the editor as-is const editorCommand = EDITOR_OVERRIDES[editor] ?? editor execSync_DEPRECATED(`${editorCommand} "${filePath}"`, { stdio: 'inherit', }) // Read the edited content const editedContent = fs.readFileSync(filePath, { encoding: 'utf-8' }) return { content: editedContent } } catch (err) { if ( typeof err === 'object' && err !== null && 'status' in err && typeof (err as { status: unknown }).status === 'number' ) { const status = (err as { status: number }).status if (status !== 0) { const editorName = toIDEDisplayName(editor) return { content: null, error: `${editorName} exited with code ${status}`, } } } return { content: null } } finally { if (useAlternateScreen) { inkInstance.exitAlternateScreen() } else { inkInstance.resumeStdin() inkInstance.resume() } } } /** * Re-collapse expanded pasted text by finding content that matches * pastedContents and replacing it with references. */ function recollapsePastedContent( editedPrompt: string, originalPrompt: string, pastedContents: Record, ): string { let collapsed = editedPrompt // Find pasted content in the edited text and re-collapse it for (const [id, content] of Object.entries(pastedContents)) { if (content.type === 'text') { const pasteId = parseInt(id) const contentStr = content.content // Check if this exact content exists in the edited prompt const contentIndex = collapsed.indexOf(contentStr) if (contentIndex !== -1) { // Replace with reference const numLines = getPastedTextRefNumLines(contentStr) const ref = formatPastedTextRef(pasteId, numLines) collapsed = collapsed.slice(0, contentIndex) + ref + collapsed.slice(contentIndex + contentStr.length) } } } return collapsed } // sync IO: called from sync context (React components, sync command handlers) export function editPromptInEditor( currentPrompt: string, pastedContents?: Record, ): EditorResult { const fs = getFsImplementation() const tempFile = generateTempFilePath() try { // Expand any pasted text references before editing const expandedPrompt = pastedContents ? expandPastedTextRefs(currentPrompt, pastedContents) : currentPrompt // Write expanded prompt to temp file writeFileSync_DEPRECATED(tempFile, expandedPrompt, { encoding: 'utf-8', flush: true, }) // Delegate to editFileInEditor const result = editFileInEditor(tempFile) if (result.content === null) { return result } // Trim a single trailing newline if present (common editor behavior) let finalContent = result.content if (finalContent.endsWith('\n') && !finalContent.endsWith('\n\n')) { finalContent = finalContent.slice(0, -1) } // Re-collapse pasted content if it wasn't edited if (pastedContents) { finalContent = recollapsePastedContent( finalContent, currentPrompt, pastedContents, ) } return { content: finalContent } } finally { // Clean up temp file try { fs.unlinkSync(tempFile) } catch { // Ignore cleanup errors } } }