Merge PR #2950: feat(coding-agent): add copy affordance to completed /btw answers (@wolfiesch)

Adds 'c copy' to the completed /btw panel footer (alongside b branch / Esc
dismiss), copying the sanitized visible answer to the clipboard. The copy
shortcut is guarded by canCopyBtw + main-editor focus + empty editor.
This commit is contained in:
can1357
2026-06-20 23:56:24 +02:00
8 changed files with 240 additions and 13 deletions
@@ -16,6 +16,7 @@ export class BtwPanelComponent extends Container {
#state: BtwPanelState = "running";
#answer = "";
#errorMessage: string | undefined;
#visibleAnswer = "";
#closed = false;
constructor(options: BtwPanelComponentOptions) {
@@ -28,12 +29,14 @@ export class BtwPanelComponent extends Container {
appendText(delta: string): void {
if (!delta || this.#closed) return;
this.#answer += delta;
this.#visibleAnswer = replaceTabs(this.#answer).trim();
this.#rebuild();
}
setAnswer(text: string): void {
if (this.#closed) return;
this.#answer = text;
this.#visibleAnswer = replaceTabs(text).trim();
this.#rebuild();
}
@@ -59,7 +62,16 @@ export class BtwPanelComponent extends Container {
}
isBranchable(): boolean {
return this.#state === "complete" && this.#answer.trim().length > 0;
return this.isCopyable();
}
isCopyable(): boolean {
return this.#state === "complete" && this.#visibleAnswer.length > 0;
}
getCopyText(): string | undefined {
if (!this.isCopyable()) return undefined;
return this.#visibleAnswer;
}
close(): void {
@@ -89,7 +101,7 @@ export class BtwPanelComponent extends Container {
case "running":
return theme.fg("muted", "Esc cancel /btw");
case "complete":
return theme.fg("muted", this.isBranchable() ? "b branch · Esc dismiss" : "Esc dismiss");
return theme.fg("muted", this.isCopyable() ? "c copy · b branch to chat · Esc dismiss" : "Esc dismiss");
case "aborted":
return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
case "error":
@@ -101,7 +113,7 @@ export class BtwPanelComponent extends Container {
if (this.#state === "error") {
return new Text(theme.fg("error", replaceTabs(this.#errorMessage ?? "Unknown error")), 1, 0);
}
const text = replaceTabs(this.#answer).trim();
const text = this.#visibleAnswer;
if (!text) {
const waiting =
this.#state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";