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:
38
cli-ink.js
38
cli-ink.js
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
85
src/ui/InkApp.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user