diff --git a/cli-ink.js b/cli-ink.js
index 8e12041..64ea5f1 100644
--- a/cli-ink.js
+++ b/cli-ink.js
@@ -2,21 +2,45 @@
import 'dotenv/config';
import React from 'react';
import { render } from 'ink';
-import InkApp from './src/ui/InkApp.js';
+import InkApp from './src/ui/InkApp.jsx';
const { unmount } = render(React.createElement(InkApp));
-// ESC to exit
+// ESC to exit (only bare ESC, not escape sequences like arrows)
if (process.stdin.isTTY) {
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 str = typeof data === 'string' ? data : String(data);
- if (str === '\u001b' || (str.length && str.charCodeAt(0) === 27)) {
- unmount();
- try { process.stdin.setRawMode(false); } catch { }
- process.exit(0);
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(String(data));
+ for (const byte of buffer) {
+ if (!escPending) {
+ if (byte === 0x1b) { // ESC
+ 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);
}
diff --git a/package.json b/package.json
index 154245f..bbcd79a 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
},
"scripts": {
"start": "node cli.js",
- "start:ink": "node cli-ink.js",
+ "start:ink": "tsx cli-ink.js",
"test": "node tests/run-all.js",
"test:patch": "node tests/run-tests.js",
"test:readfile": "node tests/run-readfile-tests.js",
diff --git a/src/ui/InkApp.js b/src/ui/InkApp.js
deleted file mode 100644
index 62e6f20..0000000
--- a/src/ui/InkApp.js
+++ /dev/null
@@ -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...'
- })
- )
- )
- );
- }
-}
-
-
diff --git a/src/ui/InkApp.jsx b/src/ui/InkApp.jsx
new file mode 100644
index 0000000..fb294cb
--- /dev/null
+++ b/src/ui/InkApp.jsx
@@ -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 (
+
+ {title}
+
+ {(lines && lines.length > 0)
+ ? lines.map((line, index) => (
+ {line}
+ ))
+ : —
+ }
+
+
+ );
+ }
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ Input:
+
+
+
+ );
+ }
+}
+
+