Refactor CLI input handling in cli-ink.js to improve ESC key detection for clean exit. Update InkApp import to .jsx extension and change start:ink script to use tsx for better compatibility. Remove deprecated InkApp.js file to streamline codebase.

This commit is contained in:
sebseb7
2025-08-12 02:56:13 +02:00
parent edf0d3cffb
commit ce6933377a
4 changed files with 117 additions and 91 deletions

View File

@@ -2,21 +2,45 @@
import 'dotenv/config'; import 'dotenv/config';
import React from 'react'; import React from 'react';
import { render } from 'ink'; import { render } from 'ink';
import InkApp from './src/ui/InkApp.js'; import InkApp from './src/ui/InkApp.jsx';
const { unmount } = render(React.createElement(InkApp)); const { unmount } = render(React.createElement(InkApp));
// ESC to exit // ESC to exit (only bare ESC, not escape sequences like arrows)
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
try { process.stdin.setRawMode(true); } catch { } try { process.stdin.setRawMode(true); } catch { }
let escPending = false;
let escTimer = null;
const exitCleanly = () => {
unmount();
try { process.stdin.setRawMode(false); } catch { }
process.exit(0);
};
const onData = (data) => { const onData = (data) => {
const str = typeof data === 'string' ? data : String(data); const buffer = Buffer.isBuffer(data) ? data : Buffer.from(String(data));
if (str === '\u001b' || (str.length && str.charCodeAt(0) === 27)) { for (const byte of buffer) {
unmount(); if (!escPending) {
try { process.stdin.setRawMode(false); } catch { } if (byte === 0x1b) { // ESC
process.exit(0); escPending = true;
escTimer = setTimeout(() => {
// No additional byte followed: treat as bare ESC
escPending = false;
escTimer = null;
exitCleanly();
}, 120);
}
// else: ignore other bytes
} else {
// Some byte followed ESC quickly: it's an escape sequence → cancel exit
if (escTimer) { clearTimeout(escTimer); escTimer = null; }
escPending = false;
// Do not process further for exit
}
} }
}; };
process.stdin.on('data', onData); process.stdin.on('data', onData);
} }

View File

@@ -53,7 +53,7 @@
}, },
"scripts": { "scripts": {
"start": "node cli.js", "start": "node cli.js",
"start:ink": "node cli-ink.js", "start:ink": "tsx cli-ink.js",
"test": "node tests/run-all.js", "test": "node tests/run-all.js",
"test:patch": "node tests/run-tests.js", "test:patch": "node tests/run-tests.js",
"test:readfile": "node tests/run-readfile-tests.js", "test:readfile": "node tests/run-readfile-tests.js",

View File

@@ -1,83 +0,0 @@
import React from 'react';
import { Box, Text } from 'ink';
import TextInput from 'ink-text-input';
class Pane extends React.Component {
render() {
const { title, lines } = this.props;
return (
React.createElement(Box, { flexDirection: 'column', borderStyle: 'round', paddingX: 1, paddingY: 0, flexGrow: 1 },
React.createElement(Text, { color: 'cyan' }, title),
React.createElement(Box, { flexDirection: 'column' },
(lines && lines.length > 0)
? lines.map((l, idx) => React.createElement(Text, { key: idx }, l))
: React.createElement(Text, { dimColor: true }, '—')
)
)
);
}
}
export default class InkApp extends React.Component {
constructor(props) {
super(props);
this.state = {
input: '',
logs: [],
terminal: [],
chainOfThought: [],
llmOutput: []
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() { }
componentWillUnmount() { }
handleChange(value) {
this.setState({ input: value });
}
handleSubmit() {
const { input } = this.state;
if (!input) return;
this.setState(state => ({
logs: [...state.logs, `> ${input}`],
// placeholders for now
terminal: [...state.terminal, `echo ${input}`],
chainOfThought: [...state.chainOfThought, `(internal) Thought about: ${input}`],
llmOutput: [...state.llmOutput, `Model says: ${input}`],
input: ''
}));
}
render() {
const { input, logs, terminal, chainOfThought, llmOutput } = this.state;
return (
React.createElement(Box, { flexDirection: 'column', height: '100%' },
React.createElement(Box, { flexGrow: 1, flexDirection: 'row' },
React.createElement(Box, { flexGrow: 1, flexDirection: 'column' },
React.createElement(Pane, { title: 'LLM Output', lines: llmOutput }),
React.createElement(Pane, { title: 'Chain of Thought', lines: chainOfThought })
),
React.createElement(Box, { flexGrow: 1, flexDirection: 'column' },
React.createElement(Pane, { title: 'Terminal', lines: terminal }),
React.createElement(Pane, { title: 'Logging', lines: logs })
)
),
React.createElement(Box, { marginTop: 1 },
React.createElement(Text, null, 'Input: '),
React.createElement(TextInput, {
value: input,
onChange: this.handleChange,
onSubmit: this.handleSubmit,
placeholder: 'Type and press Enter...'
})
)
)
);
}
}

85
src/ui/InkApp.jsx Normal file
View File

@@ -0,0 +1,85 @@
import React from 'react';
import { Box, Text } from 'ink';
import TextInput from 'ink-text-input';
class Pane extends React.Component {
render() {
const { title, lines } = this.props;
return (
<Box flexDirection="column" borderStyle="round" paddingX={1} paddingY={0} flexGrow={1}>
<Text color="cyan">{title}</Text>
<Box flexDirection="column">
{(lines && lines.length > 0)
? lines.map((line, index) => (
<Text key={index}>{line}</Text>
))
: <Text dimColor></Text>
}
</Box>
</Box>
);
}
}
export default class InkApp extends React.Component {
constructor(props) {
super(props);
this.state = {
input: '',
logs: [],
terminal: [],
chainOfThought: [],
llmOutput: []
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() { }
componentWillUnmount() { }
handleChange(value) {
this.setState({ input: value });
}
handleSubmit() {
const { input } = this.state;
if (!input) return;
this.setState((state) => ({
logs: [...state.logs, `> ${input}`],
terminal: [...state.terminal, `echo ${input}`],
chainOfThought: [...state.chainOfThought, `(internal) Thought about: ${input}`],
llmOutput: [...state.llmOutput, `Model says: ${input}`],
input: ''
}));
}
render() {
const { input, logs, terminal, chainOfThought, llmOutput } = this.state;
return (
<Box flexDirection="column" height="100%">
<Box flexGrow={1} flexDirection="row">
<Box flexGrow={1} flexDirection="column">
<Pane title="LLM Output" lines={llmOutput} />
<Pane title="Chain of Thought" lines={chainOfThought} />
</Box>
<Box flexGrow={1} flexDirection="column">
<Pane title="Terminal" lines={terminal} />
<Pane title="Logging" lines={logs} />
</Box>
</Box>
<Box marginTop={1}>
<Text>Input: </Text>
<TextInput
value={input}
onChange={this.handleChange}
onSubmit={this.handleSubmit}
placeholder="Type and press Enter..."
/>
</Box>
</Box>
);
}
}