Refactor CLI and InkApp components for improved functionality and user experience. Update model settings in InkApp, enhance terminal input handling, and streamline dependency management in package.json. Remove unused dependencies and update dotenv version for better environment variable management.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import uiService from './uiService.js';
|
||||
import TextInput from 'ink-text-input';
|
||||
import terminalService from '../terminalService.js';
|
||||
|
||||
@@ -56,25 +57,23 @@ class Pane extends React.Component {
|
||||
return out;
|
||||
}
|
||||
// Apply a blinking cursor to the given line according to width constraints
|
||||
withCursor(line, maxWidth) {
|
||||
const cursorChar = typeof this.props.cursorChar === 'string' && this.props.cursorChar.length > 0 ? this.props.cursorChar[0] : '█';
|
||||
const visible = !!this.state.cursorVisible;
|
||||
if (!visible) {
|
||||
return line;
|
||||
}
|
||||
if (typeof maxWidth === 'number' && maxWidth > 0) {
|
||||
if (line.length < maxWidth) {
|
||||
return `${line}${cursorChar}`;
|
||||
}
|
||||
if (line.length === maxWidth) {
|
||||
// Replace last char to avoid overflow
|
||||
return `${line.slice(0, maxWidth - 1)}${cursorChar}`;
|
||||
}
|
||||
// If somehow longer, just ensure width
|
||||
return `${line.slice(0, maxWidth - 1)}${cursorChar}`;
|
||||
}
|
||||
return `${line}${cursorChar}`;
|
||||
}
|
||||
withCursor(line, maxWidth) {
|
||||
const cursorChar = typeof this.props.cursorChar === 'string' && this.props.cursorChar.length > 0 ? this.props.cursorChar[0] : '█';
|
||||
const visible = !!this.state.cursorVisible;
|
||||
if (typeof maxWidth !== 'number' || maxWidth <= 0) {
|
||||
return line;
|
||||
}
|
||||
// Place cursor at the logical end of content, clamped to last column
|
||||
const width = maxWidth;
|
||||
const base = (line || '').slice(0, width);
|
||||
const cursorIndex = Math.min(base.length, width - 1);
|
||||
const targetLen = Math.min(width, cursorIndex + 1);
|
||||
const padLen = Math.max(0, targetLen - base.length);
|
||||
const padded = padLen > 0 ? `${base}${' '.repeat(padLen)}` : base.slice(0, targetLen);
|
||||
const chars = padded.split('');
|
||||
chars[cursorIndex] = visible ? cursorChar : ' ';
|
||||
return chars.join('');
|
||||
}
|
||||
render() {
|
||||
const { title, lines, maxWidth } = this.props;
|
||||
return (
|
||||
@@ -109,7 +108,12 @@ export default class InkApp extends React.Component {
|
||||
logs: [],
|
||||
terminal: [],
|
||||
chainOfThought: [],
|
||||
llmOutput: []
|
||||
llmOutput: [],
|
||||
menuOpen: false,
|
||||
menuIndex: 0,
|
||||
model: 'gpt-5',
|
||||
reasoningEffort: 'minimal',
|
||||
outputVerbosity: 'low'
|
||||
};
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
@@ -117,6 +121,9 @@ export default class InkApp extends React.Component {
|
||||
this.setChainOfThought = this.setChainOfThought.bind(this);
|
||||
this.setTerminal = this.setTerminal.bind(this);
|
||||
this.setLogs = this.setLogs.bind(this);
|
||||
this.toggleMenu = this.toggleMenu.bind(this);
|
||||
this.onKeypress = this.onKeypress.bind(this);
|
||||
this.menuAction = this.menuAction.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -141,6 +148,12 @@ export default class InkApp extends React.Component {
|
||||
process.stdout.on('resize', this.onResize);
|
||||
}
|
||||
this.onResize();
|
||||
|
||||
// Keyboard handling for menu
|
||||
if (process.stdin && process.stdin.on) {
|
||||
try { process.stdin.setRawMode(true); } catch {}
|
||||
process.stdin.on('data', this.onKeypress);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
if (this.terminalUnsub) {
|
||||
@@ -151,6 +164,9 @@ export default class InkApp extends React.Component {
|
||||
process.stdout.off('resize', this.onResize);
|
||||
this.onResize = null;
|
||||
}
|
||||
if (process.stdin && process.stdin.off) {
|
||||
process.stdin.off('data', this.onKeypress);
|
||||
}
|
||||
}
|
||||
|
||||
setPaneLines(stateKey, lines) {
|
||||
@@ -204,6 +220,82 @@ export default class InkApp extends React.Component {
|
||||
}));
|
||||
}
|
||||
|
||||
toggleMenu(open) {
|
||||
this.setState((s) => ({ menuOpen: typeof open === 'boolean' ? open : !s.menuOpen }));
|
||||
}
|
||||
|
||||
onKeypress(buf) {
|
||||
const data = Buffer.isBuffer(buf) ? buf : Buffer.from(String(buf));
|
||||
// ESC [ A => Up arrow
|
||||
const isUp = data.length >= 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
|
||||
const isDown = data.length >= 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
|
||||
const isEnter = data.length === 1 && data[0] === 0x0d;
|
||||
const isCtrlC = data.length === 1 && data[0] === 0x03;
|
||||
|
||||
if (!this.state.menuOpen) {
|
||||
if (isUp) {
|
||||
this.toggleMenu(true);
|
||||
return;
|
||||
}
|
||||
// let Ink TextInput handle normal typing; we don't intercept here
|
||||
if (isCtrlC) {
|
||||
// Pass through to exit behavior handled in cli-ink.js
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu navigation
|
||||
const items = this.getMenuItems();
|
||||
if (isUp) {
|
||||
this.setState((s) => ({ menuIndex: (s.menuIndex - 1 + items.length) % items.length }));
|
||||
return;
|
||||
}
|
||||
if (isDown) {
|
||||
this.setState((s) => ({ menuIndex: (s.menuIndex + 1) % items.length }));
|
||||
return;
|
||||
}
|
||||
if (isEnter) {
|
||||
const idx = this.state.menuIndex;
|
||||
this.menuAction(items[idx]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getMenuItems() {
|
||||
return [
|
||||
'Send CTRL-C to terminal',
|
||||
'Restart Terminal',
|
||||
'Model settings',
|
||||
'Exit the app',
|
||||
'Close menu'
|
||||
];
|
||||
}
|
||||
|
||||
menuAction(label) {
|
||||
switch (label) {
|
||||
case 'Send CTRL-C to terminal':
|
||||
try { terminalService.write('\x03'); } catch {}
|
||||
break;
|
||||
case 'Restart Terminal':
|
||||
try { terminalService.dispose(); } catch {}
|
||||
try { terminalService.start(); } catch {}
|
||||
break;
|
||||
case 'Model settings':
|
||||
// Toggle a sub-menu state
|
||||
this.setState({ menuMode: 'model' });
|
||||
break;
|
||||
case 'Exit the app':
|
||||
try { process.exit(0); } catch {}
|
||||
break;
|
||||
case 'Close menu':
|
||||
this.toggleMenu(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { input, logs, terminal, chainOfThought, llmOutput } = this.state;
|
||||
const totalCols = (process && process.stdout && process.stdout.columns) ? process.stdout.columns : 80;
|
||||
@@ -230,6 +322,9 @@ export default class InkApp extends React.Component {
|
||||
const chainOfThoughtView = sliceLast(chainOfThought);
|
||||
const terminalView = sliceLast(terminal);
|
||||
const logsView = sliceLast(logs);
|
||||
|
||||
const menuItems = this.getMenuItems();
|
||||
const selected = this.state.menuIndex;
|
||||
return (
|
||||
<Box flexDirection="column" height="100%">
|
||||
<Box flexGrow={1} flexDirection="row" minWidth={0}>
|
||||
@@ -242,6 +337,27 @@ export default class InkApp extends React.Component {
|
||||
<Pane title="Logging" lines={logsView} maxWidth={paneContentWidth} />
|
||||
</Box>
|
||||
</Box>
|
||||
{this.state.menuOpen && (
|
||||
<Box borderStyle="round" paddingX={1} paddingY={0} marginTop={1} flexDirection="column">
|
||||
<Text color="yellow">Main Menu (Up/Down to navigate, Enter to select)</Text>
|
||||
{menuItems.map((label, i) => (
|
||||
<Text key={label} color={i === selected ? 'cyan' : undefined}>
|
||||
{i === selected ? '› ' : ' '}{label}
|
||||
</Text>
|
||||
))}
|
||||
{this.state.menuMode === 'model' && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text>Model: {this.state.model}</Text>
|
||||
<Text>Reasoning effort: {this.state.reasoningEffort}</Text>
|
||||
<Text>Output verbosity: {this.state.outputVerbosity}</Text>
|
||||
<Text dimColor>
|
||||
(Adjustments pending wiring: model list [gpt-5, gpt-5-mini, gpt-5-nano, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano],
|
||||
reasoning effort [minimal, low, medium, high], output verbosity [low, medium, high])
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text>Input: </Text>
|
||||
<TextInput
|
||||
|
||||
Reference in New Issue
Block a user