Initial Release: OpenQode Public Alpha v1.3
This commit is contained in:
208
bin/ui/components/TimeoutRow.mjs
Normal file
208
bin/ui/components/TimeoutRow.mjs
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
Reference in New Issue
Block a user