Files
OpenQode/bin/ui/components/TimeoutRow.mjs
2025-12-14 00:40:14 +04:00

209 lines
6.4 KiB
JavaScript

/**
* TimeoutRow Component - "Pro" Protocol
* Interactive component for timeout recovery actions
*
* @module ui/components/TimeoutRow
*/
import React from 'react';
import { Box, Text, useInput } from 'ink';
const { useState } = React;
const h = React.createElement;
// ═══════════════════════════════════════════════════════════════
// TIMEOUT ROW - Non-destructive timeout handling
// ═══════════════════════════════════════════════════════════════
/**
* TimeoutRow Component
* Displays interactive recovery options when a request times out
*
* @param {Object} props
* @param {Function} props.onRetry - Called when user selects Retry
* @param {Function} props.onCancel - Called when user selects Cancel
* @param {Function} props.onSaveLogs - Called when user selects Save Logs
* @param {string} props.lastGoodText - Last successful text before timeout
* @param {number} props.elapsedTime - Time elapsed before timeout (seconds)
*/
export const TimeoutRow = ({
onRetry,
onCancel,
onSaveLogs,
lastGoodText = '',
elapsedTime = 120
}) => {
const [selectedAction, setSelectedAction] = useState(0);
const actions = [
{ key: 'r', label: '[R]etry', color: 'yellow', action: onRetry },
{ key: 'c', label: '[C]ancel', color: 'gray', action: onCancel },
{ key: 's', label: '[S]ave Logs', color: 'blue', action: onSaveLogs }
];
// Handle keyboard input
useInput((input, key) => {
const lowerInput = input.toLowerCase();
// Direct key shortcuts
if (lowerInput === 'r' && onRetry) {
onRetry();
return;
}
if (lowerInput === 'c' && onCancel) {
onCancel();
return;
}
if (lowerInput === 's' && onSaveLogs) {
onSaveLogs();
return;
}
// Arrow key navigation
if (key.leftArrow) {
setSelectedAction(prev => Math.max(0, prev - 1));
}
if (key.rightArrow) {
setSelectedAction(prev => Math.min(actions.length - 1, prev + 1));
}
// Enter to confirm selected action
if (key.return) {
const action = actions[selectedAction]?.action;
if (action) action();
}
});
return h(Box, {
flexDirection: 'column',
marginTop: 1,
paddingLeft: 2
},
// Warning indicator
h(Box, { marginBottom: 0 },
h(Text, { color: 'yellow', bold: true }, '⚠ '),
h(Text, { color: 'yellow' }, `Request timed out (${elapsedTime}s)`)
),
// Action buttons
h(Box, { marginTop: 0 },
...actions.map((action, i) =>
h(Box, { key: action.key, marginRight: 2 },
h(Text, {
color: i === selectedAction ? 'white' : action.color,
inverse: i === selectedAction,
bold: i === selectedAction
}, ` ${action.label} `)
)
)
),
// Context info (dimmed)
lastGoodText ? h(Box, { marginTop: 0 },
h(Text, { color: 'gray', dimColor: true },
`${lastGoodText.split('\n').length} paragraphs preserved`)
) : null
);
};
// ═══════════════════════════════════════════════════════════════
// RUN STATES - State machine for assistant responses
// ═══════════════════════════════════════════════════════════════
export const RUN_STATES = {
IDLE: 'idle',
STREAMING: 'streaming',
WAITING_FOR_TOOL: 'waiting_for_tool',
COMPLETE: 'complete',
TIMED_OUT: 'timed_out',
CANCELLED: 'cancelled'
};
/**
* Create a new Run object
* @param {string} id - Unique run ID
* @param {string} prompt - Original user prompt
* @returns {Object} New run object
*/
export function createRun(id, prompt) {
return {
id,
prompt,
state: RUN_STATES.IDLE,
partialText: '',
lastCheckpoint: '',
startTime: Date.now(),
lastActivityTime: Date.now(),
tokensReceived: 0,
error: null,
metadata: {}
};
}
/**
* Update run state with new data
* @param {Object} run - Current run object
* @param {Object} updates - Updates to apply
* @returns {Object} Updated run object
*/
export function updateRun(run, updates) {
return {
...run,
...updates,
lastActivityTime: Date.now()
};
}
/**
* Checkpoint the run for potential resume
* @param {Object} run - Current run object
* @returns {Object} Run with checkpoint set
*/
export function checkpointRun(run) {
// Find last complete paragraph for clean resume point
const text = run.partialText || '';
const paragraphs = text.split('\n\n');
const completeParagraphs = paragraphs.slice(0, -1).join('\n\n');
return {
...run,
lastCheckpoint: completeParagraphs || text
};
}
/**
* Calculate overlap for resume deduplication
* @param {string} checkpoint - Last checkpointed text
* @param {string} newText - New text from resumed generation
* @returns {string} Deduplicated combined text
*/
export function deduplicateResume(checkpoint, newText) {
if (!checkpoint || !newText) return newText || checkpoint || '';
// Find overlap at end of checkpoint / start of newText
const checkpointLines = checkpoint.split('\n');
const newLines = newText.split('\n');
// Look for matching lines to find overlap point
let overlapStart = 0;
for (let i = 0; i < newLines.length && i < 10; i++) {
const line = newLines[i].trim();
if (line && checkpointLines.some(cl => cl.trim() === line)) {
overlapStart = i + 1;
break;
}
}
// Return checkpoint + non-overlapping new content
const uniqueNewContent = newLines.slice(overlapStart).join('\n');
return checkpoint + (uniqueNewContent ? '\n\n' + uniqueNewContent : '');
}
export default {
TimeoutRow,
RUN_STATES,
createRun,
updateRun,
checkpointRun,
deduplicateResume
};