mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 15:01:26 +00:00
feat(coding-agent): add Ctrl+Z to suspend process (#267)
* feat(tui): add isCtrlZ key detection and resetRenderState method * feat(coding-agent): add Ctrl+Z handler to suspend process * docs(coding-agent): add Ctrl+Z to keyboard shortcuts documentation * feat(tui): add force parameter to requestRender
This commit is contained in:
parent
55ca650a40
commit
8868d623fc
7 changed files with 48 additions and 1 deletions
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
- **External editor support**: Press `Ctrl+G` to edit your message in an external editor. Uses `$VISUAL` or `$EDITOR` environment variable. On successful save, the message is replaced; on cancel, the original is kept. ([#266](https://github.com/badlogic/pi-mono/pull/266) by [@aliou](https://github.com/aliou))
|
- **External editor support**: Press `Ctrl+G` to edit your message in an external editor. Uses `$VISUAL` or `$EDITOR` environment variable. On successful save, the message is replaced; on cancel, the original is kept. ([#266](https://github.com/badlogic/pi-mono/pull/266) by [@aliou](https://github.com/aliou))
|
||||||
|
|
||||||
|
- **Process suspension**: Press `Ctrl+Z` to suspend pi and return to the shell. Resume with `fg` as usual. ([#267](https://github.com/badlogic/pi-mono/pull/267) by [@aliou](https://github.com/aliou))
|
||||||
|
|
||||||
## [0.25.2] - 2025-12-21
|
## [0.25.2] - 2025-12-21
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
||||||
| Escape | Cancel autocomplete / abort streaming |
|
| Escape | Cancel autocomplete / abort streaming |
|
||||||
| Ctrl+C | Clear editor (first) / exit (second) |
|
| Ctrl+C | Clear editor (first) / exit (second) |
|
||||||
| Ctrl+D | Exit (when editor is empty) |
|
| Ctrl+D | Exit (when editor is empty) |
|
||||||
|
| Ctrl+Z | Suspend to background |
|
||||||
| Shift+Tab | Cycle thinking level |
|
| Shift+Tab | Cycle thinking level |
|
||||||
| Ctrl+P | Cycle models (scoped by `--models`) |
|
| Ctrl+P | Cycle models (scoped by `--models`) |
|
||||||
| Ctrl+O | Toggle tool output expansion |
|
| Ctrl+O | Toggle tool output expansion |
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
isCtrlO,
|
isCtrlO,
|
||||||
isCtrlP,
|
isCtrlP,
|
||||||
isCtrlT,
|
isCtrlT,
|
||||||
|
isCtrlZ,
|
||||||
isEscape,
|
isEscape,
|
||||||
isShiftTab,
|
isShiftTab,
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
|
|
@ -22,6 +23,7 @@ export class CustomEditor extends Editor {
|
||||||
public onCtrlO?: () => void;
|
public onCtrlO?: () => void;
|
||||||
public onCtrlT?: () => void;
|
public onCtrlT?: () => void;
|
||||||
public onCtrlG?: () => void;
|
public onCtrlG?: () => void;
|
||||||
|
public onCtrlZ?: () => void;
|
||||||
|
|
||||||
handleInput(data: string): void {
|
handleInput(data: string): void {
|
||||||
// Intercept Ctrl+G for external editor
|
// Intercept Ctrl+G for external editor
|
||||||
|
|
@ -30,6 +32,12 @@ export class CustomEditor extends Editor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intercept Ctrl+Z for suspend
|
||||||
|
if (isCtrlZ(data) && this.onCtrlZ) {
|
||||||
|
this.onCtrlZ();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Intercept Ctrl+T for thinking block visibility toggle
|
// Intercept Ctrl+T for thinking block visibility toggle
|
||||||
if (isCtrlT(data) && this.onCtrlT) {
|
if (isCtrlT(data) && this.onCtrlT) {
|
||||||
this.onCtrlT();
|
this.onCtrlT();
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,9 @@ export class InteractiveMode {
|
||||||
theme.fg("dim", "ctrl+d") +
|
theme.fg("dim", "ctrl+d") +
|
||||||
theme.fg("muted", " to exit (empty)") +
|
theme.fg("muted", " to exit (empty)") +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
theme.fg("dim", "ctrl+z") +
|
||||||
|
theme.fg("muted", " to suspend") +
|
||||||
|
"\n" +
|
||||||
theme.fg("dim", "ctrl+k") +
|
theme.fg("dim", "ctrl+k") +
|
||||||
theme.fg("muted", " to delete line") +
|
theme.fg("muted", " to delete line") +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
|
@ -576,6 +579,7 @@ export class InteractiveMode {
|
||||||
|
|
||||||
this.editor.onCtrlC = () => this.handleCtrlC();
|
this.editor.onCtrlC = () => this.handleCtrlC();
|
||||||
this.editor.onCtrlD = () => this.handleCtrlD();
|
this.editor.onCtrlD = () => this.handleCtrlD();
|
||||||
|
this.editor.onCtrlZ = () => this.handleCtrlZ();
|
||||||
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
||||||
this.editor.onCtrlP = () => this.cycleModel();
|
this.editor.onCtrlP = () => this.cycleModel();
|
||||||
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
||||||
|
|
@ -1159,6 +1163,20 @@ export class InteractiveMode {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleCtrlZ(): void {
|
||||||
|
// Set up handler to restore TUI when resumed
|
||||||
|
process.once("SIGCONT", () => {
|
||||||
|
this.ui.start();
|
||||||
|
this.ui.requestRender(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop the TUI (restore terminal to normal mode)
|
||||||
|
this.ui.stop();
|
||||||
|
|
||||||
|
// Send SIGTSTP to process group (pid=0 means all processes in group)
|
||||||
|
process.kill(0, "SIGTSTP");
|
||||||
|
}
|
||||||
|
|
||||||
private updateEditorBorderColor(): void {
|
private updateEditorBorderColor(): void {
|
||||||
if (this.isBashMode) {
|
if (this.isBashMode) {
|
||||||
this.editor.borderColor = theme.getBashModeBorderColor();
|
this.editor.borderColor = theme.getBashModeBorderColor();
|
||||||
|
|
@ -1747,6 +1765,7 @@ export class InteractiveMode {
|
||||||
| \`Escape\` | Cancel autocomplete / abort streaming |
|
| \`Escape\` | Cancel autocomplete / abort streaming |
|
||||||
| \`Ctrl+C\` | Clear editor (first) / exit (second) |
|
| \`Ctrl+C\` | Clear editor (first) / exit (second) |
|
||||||
| \`Ctrl+D\` | Exit (when editor is empty) |
|
| \`Ctrl+D\` | Exit (when editor is empty) |
|
||||||
|
| \`Ctrl+Z\` | Suspend to background |
|
||||||
| \`Shift+Tab\` | Cycle thinking level |
|
| \`Shift+Tab\` | Cycle thinking level |
|
||||||
| \`Ctrl+P\` | Cycle models |
|
| \`Ctrl+P\` | Cycle models |
|
||||||
| \`Ctrl+O\` | Toggle tool output expansion |
|
| \`Ctrl+O\` | Toggle tool output expansion |
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export {
|
||||||
isCtrlT,
|
isCtrlT,
|
||||||
isCtrlU,
|
isCtrlU,
|
||||||
isCtrlW,
|
isCtrlW,
|
||||||
|
isCtrlZ,
|
||||||
isDelete,
|
isDelete,
|
||||||
isEnd,
|
isEnd,
|
||||||
isEnter,
|
isEnter,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ const CODEPOINTS = {
|
||||||
t: 116,
|
t: 116,
|
||||||
u: 117,
|
u: 117,
|
||||||
w: 119,
|
w: 119,
|
||||||
|
z: 122,
|
||||||
|
|
||||||
// Special keys
|
// Special keys
|
||||||
escape: 27,
|
escape: 27,
|
||||||
|
|
@ -168,6 +169,7 @@ export const Keys = {
|
||||||
CTRL_T: kittySequence(CODEPOINTS.t, MODIFIERS.ctrl),
|
CTRL_T: kittySequence(CODEPOINTS.t, MODIFIERS.ctrl),
|
||||||
CTRL_U: kittySequence(CODEPOINTS.u, MODIFIERS.ctrl),
|
CTRL_U: kittySequence(CODEPOINTS.u, MODIFIERS.ctrl),
|
||||||
CTRL_W: kittySequence(CODEPOINTS.w, MODIFIERS.ctrl),
|
CTRL_W: kittySequence(CODEPOINTS.w, MODIFIERS.ctrl),
|
||||||
|
CTRL_Z: kittySequence(CODEPOINTS.z, MODIFIERS.ctrl),
|
||||||
|
|
||||||
// Enter combinations
|
// Enter combinations
|
||||||
SHIFT_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.shift),
|
SHIFT_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.shift),
|
||||||
|
|
@ -223,6 +225,7 @@ const RAW = {
|
||||||
CTRL_T: "\x14",
|
CTRL_T: "\x14",
|
||||||
CTRL_U: "\x15",
|
CTRL_U: "\x15",
|
||||||
CTRL_W: "\x17",
|
CTRL_W: "\x17",
|
||||||
|
CTRL_Z: "\x1a",
|
||||||
ALT_BACKSPACE: "\x1b\x7f",
|
ALT_BACKSPACE: "\x1b\x7f",
|
||||||
SHIFT_TAB: "\x1b[Z",
|
SHIFT_TAB: "\x1b[Z",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
@ -322,6 +325,14 @@ export function isCtrlW(data: string): boolean {
|
||||||
return data === RAW.CTRL_W || data === Keys.CTRL_W || matchesKittySequence(data, CODEPOINTS.w, MODIFIERS.ctrl);
|
return data === RAW.CTRL_W || data === Keys.CTRL_W || matchesKittySequence(data, CODEPOINTS.w, MODIFIERS.ctrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if input matches Ctrl+Z (raw byte or Kitty protocol).
|
||||||
|
* Ignores lock key bits.
|
||||||
|
*/
|
||||||
|
export function isCtrlZ(data: string): boolean {
|
||||||
|
return data === RAW.CTRL_Z || data === Keys.CTRL_Z || matchesKittySequence(data, CODEPOINTS.z, MODIFIERS.ctrl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if input matches Alt+Backspace (legacy or Kitty protocol).
|
* Check if input matches Alt+Backspace (legacy or Kitty protocol).
|
||||||
* Ignores lock key bits.
|
* Ignores lock key bits.
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,12 @@ export class TUI extends Container {
|
||||||
this.terminal.stop();
|
this.terminal.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
requestRender(): void {
|
requestRender(force = false): void {
|
||||||
|
if (force) {
|
||||||
|
this.previousLines = [];
|
||||||
|
this.previousWidth = 0;
|
||||||
|
this.cursorRow = 0;
|
||||||
|
}
|
||||||
if (this.renderRequested) return;
|
if (this.renderRequested) return;
|
||||||
this.renderRequested = true;
|
this.renderRequested = true;
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue