mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 20:04:55 +00:00
feat(tui): improve editor Ctrl/Alt word and line deletion shortcuts
- Add Ctrl+W for word deletion (stops at whitespace/punctuation) - Add Ctrl+U for delete to start of line (merges with previous line at col 0) - Change Ctrl+K from delete entire line to delete to end of line (merges with next line at EOL) - Add Option+Backspace support in Ghostty (maps to Ctrl+W via ESC+DEL sequence) - Cmd+Backspace in Ghostty works as Ctrl+U (terminal sends same control code) - Update README and CHANGELOG with new keyboard shortcuts Fixes #2, Fixes #3
This commit is contained in:
parent
508d1bb2d6
commit
a686f61c1d
6 changed files with 215 additions and 36 deletions
|
|
@ -11,3 +11,4 @@
|
||||||
- Always run `npm run check` in the project's root directory after making code changes.
|
- Always run `npm run check` in the project's root directory after making code changes.
|
||||||
- You must NEVER run `npm run dev` yourself. Doing is means you failed the user hard.
|
- You must NEVER run `npm run dev` yourself. Doing is means you failed the user hard.
|
||||||
- Do NOT commit unless asked to by the user
|
- Do NOT commit unless asked to by the user
|
||||||
|
- Keep you answers short and concise and to the point.
|
||||||
|
|
@ -1898,9 +1898,9 @@ export const MODELS = {
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 1.5,
|
input: 0.25,
|
||||||
output: 6,
|
output: 2,
|
||||||
cacheRead: 0.375,
|
cacheRead: 0.024999999999999998,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 400000,
|
contextWindow: 400000,
|
||||||
|
|
@ -2289,13 +2289,13 @@ export const MODELS = {
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.6,
|
input: 0.44999999999999996,
|
||||||
output: 2.2,
|
output: 1.9,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 204800,
|
contextWindow: 202752,
|
||||||
maxTokens: 131072,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"anthropic/claude-sonnet-4.5": {
|
"anthropic/claude-sonnet-4.5": {
|
||||||
id: "anthropic/claude-sonnet-4.5",
|
id: "anthropic/claude-sonnet-4.5",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Editor: updated keyboard shortcuts to follow Unix conventions:
|
||||||
|
- **Ctrl+W** deletes the previous word (stopping at whitespace or punctuation)
|
||||||
|
- **Ctrl+U** deletes from cursor to start of line (at line start, merges with previous line)
|
||||||
|
- **Ctrl+K** deletes from cursor to end of line (at line end, merges with next line)
|
||||||
|
- **Option+Backspace** in Ghostty now behaves like **Ctrl+W** (delete word backwards)
|
||||||
|
- **Cmd+Backspace** in Ghostty now behaves like **Ctrl+U** (delete to start of line)
|
||||||
|
|
||||||
## [0.7.8] - 2025-11-13
|
## [0.7.8] - 2025-11-13
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,11 @@ Paste multiple lines of text (e.g., code snippets, logs) and they'll be automati
|
||||||
|
|
||||||
### Keyboard Shortcuts
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
- **Ctrl+K**: Delete current line
|
- **Ctrl+W**: Delete word backwards (stops at whitespace or punctuation)
|
||||||
|
- **Option+Backspace** (Ghostty): Delete word backwards (same as Ctrl+W)
|
||||||
|
- **Ctrl+U**: Delete to start of line (at line start: merge with previous line)
|
||||||
|
- **Cmd+Backspace** (Ghostty): Delete to start of line (same as Ctrl+U)
|
||||||
|
- **Ctrl+K**: Delete to end of line (at line end: merge with next line)
|
||||||
- **Ctrl+C**: Clear editor (first press) / Exit pi (second press)
|
- **Ctrl+C**: Clear editor (first press) / Exit pi (second press)
|
||||||
- **Tab**: Path completion
|
- **Tab**: Path completion
|
||||||
- **Enter**: Send message
|
- **Enter**: Send message
|
||||||
|
|
|
||||||
|
|
@ -231,9 +231,21 @@ export class Editor implements Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with rest of input handling
|
// Continue with rest of input handling
|
||||||
// Ctrl+K - Delete current line
|
// Ctrl+K - Delete to end of line
|
||||||
if (data.charCodeAt(0) === 11) {
|
if (data.charCodeAt(0) === 11) {
|
||||||
this.deleteCurrentLine();
|
this.deleteToEndOfLine();
|
||||||
|
}
|
||||||
|
// Ctrl+U - Delete to start of line
|
||||||
|
else if (data.charCodeAt(0) === 21) {
|
||||||
|
this.deleteToStartOfLine();
|
||||||
|
}
|
||||||
|
// Ctrl+W - Delete word backwards
|
||||||
|
else if (data.charCodeAt(0) === 23) {
|
||||||
|
this.deleteWordBackwards();
|
||||||
|
}
|
||||||
|
// Option/Alt+Backspace (e.g. Ghostty sends ESC + DEL)
|
||||||
|
else if (data === "\x1b\x7f") {
|
||||||
|
this.deleteWordBackwards();
|
||||||
}
|
}
|
||||||
// Ctrl+A - Move to start of line
|
// Ctrl+A - Move to start of line
|
||||||
else if (data.charCodeAt(0) === 1) {
|
else if (data.charCodeAt(0) === 1) {
|
||||||
|
|
@ -598,6 +610,93 @@ export class Editor implements Component {
|
||||||
this.state.cursorCol = currentLine.length;
|
this.state.cursorCol = currentLine.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private deleteToStartOfLine(): void {
|
||||||
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
||||||
|
|
||||||
|
if (this.state.cursorCol > 0) {
|
||||||
|
// Delete from start of line up to cursor
|
||||||
|
this.state.lines[this.state.cursorLine] = currentLine.slice(this.state.cursorCol);
|
||||||
|
this.state.cursorCol = 0;
|
||||||
|
} else if (this.state.cursorLine > 0) {
|
||||||
|
// At start of line - merge with previous line
|
||||||
|
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
||||||
|
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
||||||
|
this.state.lines.splice(this.state.cursorLine, 1);
|
||||||
|
this.state.cursorLine--;
|
||||||
|
this.state.cursorCol = previousLine.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(this.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteToEndOfLine(): void {
|
||||||
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
||||||
|
|
||||||
|
if (this.state.cursorCol < currentLine.length) {
|
||||||
|
// Delete from cursor to end of line
|
||||||
|
this.state.lines[this.state.cursorLine] = currentLine.slice(0, this.state.cursorCol);
|
||||||
|
} else if (this.state.cursorLine < this.state.lines.length - 1) {
|
||||||
|
// At end of line - merge with next line
|
||||||
|
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
||||||
|
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
||||||
|
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(this.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteWordBackwards(): void {
|
||||||
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
||||||
|
|
||||||
|
// If at start of line, behave like backspace at column 0 (merge with previous line)
|
||||||
|
if (this.state.cursorCol === 0) {
|
||||||
|
if (this.state.cursorLine > 0) {
|
||||||
|
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
||||||
|
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
||||||
|
this.state.lines.splice(this.state.cursorLine, 1);
|
||||||
|
this.state.cursorLine--;
|
||||||
|
this.state.cursorCol = previousLine.length;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
||||||
|
|
||||||
|
const isWhitespace = (char: string): boolean => /\s/.test(char);
|
||||||
|
const isPunctuation = (char: string): boolean => {
|
||||||
|
// Treat obvious code punctuation as boundaries
|
||||||
|
return /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/.test(char);
|
||||||
|
};
|
||||||
|
|
||||||
|
let deleteFrom = this.state.cursorCol;
|
||||||
|
const lastChar = textBeforeCursor[deleteFrom - 1] ?? "";
|
||||||
|
|
||||||
|
// If immediately on whitespace or punctuation, delete that single boundary char
|
||||||
|
if (isWhitespace(lastChar) || isPunctuation(lastChar)) {
|
||||||
|
deleteFrom -= 1;
|
||||||
|
} else {
|
||||||
|
// Otherwise, delete a run of non-boundary characters (the "word")
|
||||||
|
while (deleteFrom > 0) {
|
||||||
|
const ch = textBeforeCursor[deleteFrom - 1] ?? "";
|
||||||
|
if (isWhitespace(ch) || isPunctuation(ch)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
deleteFrom -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.lines[this.state.cursorLine] =
|
||||||
|
currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
|
||||||
|
this.state.cursorCol = deleteFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(this.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleForwardDelete(): void {
|
private handleForwardDelete(): void {
|
||||||
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
||||||
|
|
||||||
|
|
@ -618,31 +717,6 @@ export class Editor implements Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteCurrentLine(): void {
|
|
||||||
if (this.state.lines.length === 1) {
|
|
||||||
// Only one line - just clear it
|
|
||||||
this.state.lines[0] = "";
|
|
||||||
this.state.cursorCol = 0;
|
|
||||||
} else {
|
|
||||||
// Multiple lines - remove current line
|
|
||||||
this.state.lines.splice(this.state.cursorLine, 1);
|
|
||||||
|
|
||||||
// Adjust cursor position
|
|
||||||
if (this.state.cursorLine >= this.state.lines.length) {
|
|
||||||
// Was on last line, move to new last line
|
|
||||||
this.state.cursorLine = this.state.lines.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp cursor column to new line length
|
|
||||||
const newLine = this.state.lines[this.state.cursorLine] || "";
|
|
||||||
this.state.cursorCol = Math.min(this.state.cursorCol, newLine.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.onChange) {
|
|
||||||
this.onChange(this.getText());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private moveCursor(deltaLine: number, deltaCol: number): void {
|
private moveCursor(deltaLine: number, deltaCol: number): void {
|
||||||
if (deltaLine !== 0) {
|
if (deltaLine !== 0) {
|
||||||
const newLine = this.state.cursorLine + deltaLine;
|
const newLine = this.state.cursorLine + deltaLine;
|
||||||
|
|
|
||||||
91
packages/tui/test/key-tester.ts
Executable file
91
packages/tui/test/key-tester.ts
Executable file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import { ProcessTerminal } from "../src/terminal.js";
|
||||||
|
import { type Component, TUI } from "../src/tui.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple key code logger component
|
||||||
|
*/
|
||||||
|
class KeyLogger implements Component {
|
||||||
|
private log: string[] = [];
|
||||||
|
private maxLines = 20;
|
||||||
|
private tui: TUI;
|
||||||
|
|
||||||
|
constructor(tui: TUI) {
|
||||||
|
this.tui = tui;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInput(data: string): void {
|
||||||
|
// Convert to various representations
|
||||||
|
const hex = Buffer.from(data).toString("hex");
|
||||||
|
const charCodes = Array.from(data)
|
||||||
|
.map((c) => c.charCodeAt(0))
|
||||||
|
.join(", ");
|
||||||
|
const repr = data
|
||||||
|
.replace(/\x1b/g, "\\x1b")
|
||||||
|
.replace(/\r/g, "\\r")
|
||||||
|
.replace(/\n/g, "\\n")
|
||||||
|
.replace(/\t/g, "\\t")
|
||||||
|
.replace(/\x7f/g, "\\x7f");
|
||||||
|
|
||||||
|
const logLine = `Hex: ${hex.padEnd(20)} | Chars: [${charCodes.padEnd(15)}] | Repr: "${repr}"`;
|
||||||
|
|
||||||
|
this.log.push(logLine);
|
||||||
|
|
||||||
|
// Keep only last N lines
|
||||||
|
if (this.log.length > this.maxLines) {
|
||||||
|
this.log.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request re-render to show the new log entry
|
||||||
|
this.tui.requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(width: number): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// Title
|
||||||
|
lines.push("=".repeat(width));
|
||||||
|
lines.push("Key Code Tester - Press keys to see their codes (Ctrl+C to exit)".padEnd(width));
|
||||||
|
lines.push("=".repeat(width));
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
// Log entries
|
||||||
|
for (const entry of this.log) {
|
||||||
|
lines.push(entry.padEnd(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill remaining space
|
||||||
|
const remaining = Math.max(0, 25 - lines.length);
|
||||||
|
for (let i = 0; i < remaining; i++) {
|
||||||
|
lines.push("".padEnd(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
lines.push("=".repeat(width));
|
||||||
|
lines.push("Test these:".padEnd(width));
|
||||||
|
lines.push(" - Option/Alt + Backspace".padEnd(width));
|
||||||
|
lines.push(" - Cmd/Ctrl + Backspace".padEnd(width));
|
||||||
|
lines.push(" - Regular Backspace".padEnd(width));
|
||||||
|
lines.push("=".repeat(width));
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up TUI
|
||||||
|
const terminal = new ProcessTerminal();
|
||||||
|
const tui = new TUI(terminal);
|
||||||
|
const logger = new KeyLogger(tui);
|
||||||
|
|
||||||
|
tui.addChild(logger);
|
||||||
|
tui.setFocus(logger);
|
||||||
|
|
||||||
|
// Handle Ctrl+C for clean exit
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
tui.stop();
|
||||||
|
console.log("\nExiting...");
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the TUI
|
||||||
|
tui.start();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue