add cross-platform clipboard support and /copy command

This commit is contained in:
Markus Ylisiurunen 2025-12-03 23:43:02 +02:00
parent db6d655ee9
commit 667e7aa730
2 changed files with 82 additions and 0 deletions

View file

@ -0,0 +1,28 @@
import { execSync } from "child_process";
import { platform } from "os";
export function copyToClipboard(text: string): void {
const p = platform();
const options = { input: text, timeout: 5000 };
try {
if (p === "darwin") {
execSync("pbcopy", options);
} else if (p === "win32") {
execSync("clip", options);
} else {
// Linux - try xclip first, fall back to xsel
try {
execSync("xclip -selection clipboard", options);
} catch {
execSync("xsel --clipboard --input", options);
}
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (p === "linux") {
throw new Error(`Failed to copy to clipboard. Install xclip or xsel: ${msg}`);
}
throw new Error(`Failed to copy to clipboard: ${msg}`);
}
}

View file

@ -18,6 +18,7 @@ import {
} from "@mariozechner/pi-tui";
import { exec } from "child_process";
import { getChangelogPath, parseChangelog } from "../changelog.js";
import { copyToClipboard } from "../clipboard.js";
import { calculateContextTokens, compact, shouldCompact } from "../compaction.js";
import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js";
import { exportSessionToHtml } from "../export-html.js";
@ -155,6 +156,11 @@ export class TuiRenderer {
description: "Export session to HTML file",
};
const copyCommand: SlashCommand = {
name: "copy",
description: "Copy last agent message to clipboard",
};
const sessionCommand: SlashCommand = {
name: "session",
description: "Show session info and stats",
@ -221,6 +227,7 @@ export class TuiRenderer {
modelCommand,
themeCommand,
exportCommand,
copyCommand,
sessionCommand,
changelogCommand,
branchCommand,
@ -383,6 +390,13 @@ export class TuiRenderer {
return;
}
// Check for /copy command
if (text === "/copy") {
this.handleCopyCommand();
this.editor.setText("");
return;
}
// Check for /session command
if (text === "/session") {
this.handleSessionCommand();
@ -1573,6 +1587,46 @@ export class TuiRenderer {
}
}
private handleCopyCommand(): void {
// Find the last assistant message
const lastAssistantMessage = this.agent.state.messages
.slice()
.reverse()
.find((m) => m.role === "assistant");
if (!lastAssistantMessage) {
this.showError("No agent messages to copy yet.");
return;
}
// Extract raw text content from all text blocks
let textContent = "";
for (const content of lastAssistantMessage.content) {
if (content.type === "text") {
textContent += content.text;
}
}
if (!textContent.trim()) {
this.showError("Last agent message contains no text content.");
return;
}
// Copy to clipboard using cross-platform compatible method
try {
copyToClipboard(textContent);
} catch (error) {
this.showError(error instanceof Error ? error.message : String(error));
return;
}
// Show confirmation message
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("dim", "Copied last agent message to clipboard"), 1, 0));
this.ui.requestRender();
}
private handleSessionCommand(): void {
// Get session info
const sessionFile = this.sessionManager.getSessionFile();