From ce6933377a7093b7550d5fb5fe56455378620b4f Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Tue, 12 Aug 2025 02:56:13 +0200 Subject: [PATCH] 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. --- cli-ink.js | 38 +++++++++++++++++---- package.json | 2 +- src/ui/InkApp.js | 83 --------------------------------------------- src/ui/InkApp.jsx | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 91 deletions(-) delete mode 100644 src/ui/InkApp.js create mode 100644 src/ui/InkApp.jsx 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: + + + + ); + } +} + +