Files
OpenQode/bin/ui/components/RunStrip.mjs

150 lines
4.0 KiB
JavaScript

/**
* RunStrip Component
*
* SINGLE STATE SURFACE: One place for all run state
* - thinking / streaming / waiting / failed / idle
*
* DESIGN:
* - Compact single-line strip at top of main panel
* - Shows: state • agent • model • cwd
* - Never reflows, fixed height (1 row)
*/
import React from 'react';
import { Box, Text } from 'ink';
import Spinner from 'ink-spinner';
import { colors } from '../../tui-theme.mjs';
import { icon, statusIcon } from '../../icons.mjs';
import { getCapabilities } from '../../terminal-profile.mjs';
const h = React.createElement;
// Run states
const RUN_STATES = {
IDLE: 'idle',
THINKING: 'thinking',
STREAMING: 'streaming',
WAITING: 'waiting',
TOOL: 'tool',
FAILED: 'failed',
SUCCESS: 'success'
};
/**
* Get state display info
*/
const getStateDisplay = (state, message) => {
const caps = getCapabilities();
const displays = {
[RUN_STATES.IDLE]: {
icon: caps.unicodeOK ? '●' : '*',
color: colors.success,
text: 'Ready'
},
[RUN_STATES.THINKING]: {
icon: null, // spinner
color: 'yellow',
text: message || 'Thinking...',
showSpinner: true
},
[RUN_STATES.STREAMING]: {
icon: null,
color: colors.accent,
text: message || 'Generating...',
showSpinner: true
},
[RUN_STATES.WAITING]: {
icon: caps.unicodeOK ? '◐' : '~',
color: 'yellow',
text: message || 'Waiting...'
},
[RUN_STATES.TOOL]: {
icon: null,
color: 'magenta',
text: message || 'Running tool...',
showSpinner: true
},
[RUN_STATES.FAILED]: {
icon: caps.unicodeOK ? '✗' : 'X',
color: colors.error,
text: message || 'Failed'
},
[RUN_STATES.SUCCESS]: {
icon: caps.unicodeOK ? '✓' : '+',
color: colors.success,
text: message || 'Done'
}
};
return displays[state] || displays[RUN_STATES.IDLE];
};
/**
* RunStrip - Compact run state indicator
*
* Props:
* - state: one of RUN_STATES
* - message: optional status message
* - agent: current agent name
* - model: current model name
* - tokensPerSec: streaming speed
* - width: strip width
*/
const RunStrip = ({
state = RUN_STATES.IDLE,
message = null,
agent = null,
model = null,
tokensPerSec = 0,
width = 80
}) => {
const caps = getCapabilities();
const display = getStateDisplay(state, message);
const separator = caps.unicodeOK ? '│' : '|';
// Build parts
const parts = [];
// State indicator
if (display.showSpinner) {
parts.push(h(Spinner, { key: 'spin', type: 'dots' }));
parts.push(h(Text, { key: 'space1' }, ' '));
} else if (display.icon) {
parts.push(h(Text, { key: 'icon', color: display.color }, display.icon + ' '));
}
// State text
parts.push(h(Text, { key: 'state', color: display.color }, display.text));
// Tokens per second (only when streaming)
if (state === RUN_STATES.STREAMING && tokensPerSec > 0) {
parts.push(h(Text, { key: 'tps', color: colors.muted }, ` ${tokensPerSec} tok/s`));
}
// Separator + Agent
if (agent) {
parts.push(h(Text, { key: 'sep1', color: colors.muted }, ` ${separator} `));
parts.push(h(Text, { key: 'agent', color: colors.muted }, agent.toUpperCase()));
}
// Separator + Model
if (model) {
parts.push(h(Text, { key: 'sep2', color: colors.muted }, ` ${separator} `));
parts.push(h(Text, { key: 'model', color: colors.muted, dimColor: true },
model.length > 20 ? model.slice(0, 18) + '…' : model
));
}
return h(Box, {
flexDirection: 'row',
width: width,
height: 1,
flexShrink: 0,
paddingX: 1
}, ...parts);
};
export default RunStrip;
export { RunStrip, RUN_STATES, getStateDisplay };