feat: add text clipboard copy/read with mouse selection support

Add cross-platform text clipboard operations (macOS, Linux, Windows)
with OSC 52 support for SSH/tmux environments. Wire up onMouseUp and
Ctrl+Y in the TUI to copy selected text to the system clipboard via
OpenTUI's renderer selection API.
This commit is contained in:
2026-02-06 11:01:08 -05:00
parent 8adf48abd3
commit 101300b103
17 changed files with 983 additions and 87 deletions

View File

@@ -0,0 +1,23 @@
/**
* Clipboard constants for text copy/read operations
*/
/** OSC 52 escape sequence for clipboard write (insert base64 payload) */
export const OSC52_SEQUENCE_PREFIX = "\x1b]52;c;";
export const OSC52_SEQUENCE_SUFFIX = "\x07";
/** DCS tmux passthrough wrapping */
export const TMUX_DCS_PREFIX = "\x1bPtmux;\x1b";
export const TMUX_DCS_SUFFIX = "\x1b\\";
/** Environment variables indicating tmux/screen session */
export const TMUX_ENV_VAR = "TMUX";
export const SCREEN_ENV_VAR = "STY";
export const WAYLAND_DISPLAY_ENV_VAR = "WAYLAND_DISPLAY";
/** Platform command timeout in milliseconds */
export const CLIPBOARD_COMMAND_TIMEOUT_MS = 5000;
/** Supported MIME types for clipboard content */
export const CLIPBOARD_MIME_TEXT = "text/plain" as const;
export const CLIPBOARD_MIME_IMAGE_PNG = "image/png" as const;

View File

@@ -26,11 +26,38 @@ export const PARTIAL_X10_REGEX = /\x1b\[M.{0,2}$/;
// Mouse tracking escape sequences
export const MOUSE_TRACKING_SEQUENCES = {
ENABLE_BUTTON: "\x1b[?1000h",
ENABLE_BUTTON_EVENT: "\x1b[?1002h",
ENABLE_SGR: "\x1b[?1006h",
DISABLE_SGR: "\x1b[?1006l",
DISABLE_BUTTON_EVENT: "\x1b[?1002l",
DISABLE_BUTTON: "\x1b[?1000l",
} as const;
// Enable button-event tracking + SGR encoding (for scroll + drag selection)
export const MOUSE_TRACKING_ENABLE =
MOUSE_TRACKING_SEQUENCES.ENABLE_BUTTON_EVENT +
MOUSE_TRACKING_SEQUENCES.ENABLE_SGR;
// Mouse button action codes (SGR encoding)
export const MOUSE_BUTTON = {
LEFT_PRESS: 0,
MIDDLE_PRESS: 1,
RIGHT_PRESS: 2,
LEFT_DRAG: 32,
MIDDLE_DRAG: 33,
RIGHT_DRAG: 34,
SCROLL_UP: 64,
SCROLL_DOWN: 65,
} as const;
// Disable button-event tracking + SGR encoding
export const MOUSE_TRACKING_DISABLE =
MOUSE_TRACKING_SEQUENCES.DISABLE_BUTTON_EVENT +
MOUSE_TRACKING_SEQUENCES.DISABLE_SGR;
// Time in ms to re-enable mouse tracking after selection ends
export const MOUSE_SELECTION_REENABLE_MS = 2000;
// Scroll direction type
export type MouseScrollDirection = "up" | "down";

View File

@@ -0,0 +1,58 @@
/**
* Plan Approval Constants
*
* Options and configuration for the plan approval prompt,
* modeled after Claude Code's plan approval flow.
*/
/** How edits should be handled after plan approval */
export type PlanEditMode =
| "auto_accept_clear" // Clear context and auto-accept edits
| "auto_accept" // Auto-accept edits
| "manual_approve" // Manually approve each edit
| "feedback"; // User provides feedback/changes
/** A selectable option in the plan approval modal */
export interface PlanApprovalOption {
key: string;
label: string;
description: string;
editMode: PlanEditMode;
shortcut?: string;
}
/** Available plan approval options, matching Claude Code's pattern */
export const PLAN_APPROVAL_OPTIONS: PlanApprovalOption[] = [
{
key: "1",
label: "Yes, clear context and auto-accept edits",
description: "Approve plan, clear conversation context, and auto-accept all file edits",
editMode: "auto_accept_clear",
shortcut: "shift+tab",
},
{
key: "2",
label: "Yes, auto-accept edits",
description: "Approve plan and auto-accept all file edits without prompting",
editMode: "auto_accept",
},
{
key: "3",
label: "Yes, manually approve edits",
description: "Approve plan but require manual approval for each file edit",
editMode: "manual_approve",
},
{
key: "4",
label: "Type here to tell CodeTyper what to change",
description: "Provide feedback or modifications to the plan",
editMode: "feedback",
},
];
/** Maximum visible options before scrolling */
export const PLAN_APPROVAL_MAX_VISIBLE_OPTIONS = 4;
/** Footer help text for plan approval modal */
export const PLAN_APPROVAL_FOOTER_TEXT =
"ctrl-g to edit in editor";

View File

@@ -7,3 +7,14 @@ export const DISABLE_MOUSE_TRACKING =
"\x1b[?1003l" + // All mouse events (motion)
"\x1b[?1002l" + // Button event mouse tracking
"\x1b[?1000l"; // Normal tracking mode
/**
* Full terminal reset sequence for cleanup on exit
* Disables all tracking modes, restores cursor, exits alternate screen
*/
export const TERMINAL_RESET =
DISABLE_MOUSE_TRACKING +
"\x1b[?25h" + // Show cursor
"\x1b[?1049l" + // Leave alternate screen
"\x1b[>4;0m" + // Disable Kitty keyboard protocol (pop)
"\x1b[?2004l"; // Disable bracketed paste mode

View File

@@ -0,0 +1,30 @@
/**
* Thinking Tags Constants
*
* XML tag names that LLM providers use for reasoning/thinking blocks.
* These should be stripped from visible streaming output and displayed
* as dimmed thinking indicators instead.
*/
/** Tag names to strip from streaming content */
export const THINKING_TAGS = [
"thinking",
"search",
"plan",
"execute",
] as const;
export type ThinkingTagName = (typeof THINKING_TAGS)[number];
/** Regex pattern to match a complete opening tag: <thinking>, <search>, etc. */
export const THINKING_OPEN_TAG_REGEX = new RegExp(
`<(${THINKING_TAGS.join("|")})>`,
);
/** Regex pattern to match a complete closing tag: </thinking>, </search>, etc. */
export const THINKING_CLOSE_TAG_REGEX = new RegExp(
`</(${THINKING_TAGS.join("|")})>`,
);
/** Maximum buffer size before flushing as visible text (safety valve) */
export const THINKING_BUFFER_MAX_SIZE = 256;