150 lines
4.0 KiB
JavaScript
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 };
|