Replace fake TUI buttons with usage instructions

This commit is contained in:
Matteo Benedetto
2026-09-23 10:50:28 +02:00
parent e68d8e7f2e
commit cf063fd8aa
2 changed files with 13 additions and 21 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
# pi-bash-code-actions # pi-bash-code-actions
A Pi extension that detects fenced `bash`, `sh`, `shell`, and `zsh` blocks in assistant replies and adds actions to the transcript: A Pi extension that detects fenced `bash`, `sh`, `shell`, and `zsh` blocks in assistant replies and shows an informational note in the transcript. Use `/bash-blocks` to choose an action:
- **Copy** the block to the system clipboard (`wl-copy`, `xclip`, or `xsel` on Linux; `pbcopy` on macOS; `clip` on Windows). - **Copy** the block to the system clipboard (`wl-copy`, `xclip`, or `xsel` on Linux; `pbcopy` on macOS; `clip` on Windows).
- **Open terminal** with the block loaded as a temporary script. It prefers Konsole, then `$TERMINAL`, GNOME Terminal, and `x-terminal-emulator`. - **Open terminal** with the block loaded as a temporary script. It prefers Konsole, then `$TERMINAL`, GNOME Terminal, and `x-terminal-emulator`.
- **Execute** after showing the code in a Pi confirmation dialog. Execution takes place in the newly opened terminal. - **Execute** after showing the code in a Pi confirmation dialog. Execution takes place in the newly opened terminal.
The `/bash-blocks` command lets you select a detected block and choose an action. Existing session history is scanned when the extension starts. The transcript note is informational; it is not clickable. The `/bash-blocks` command lets you select a detected block and choose an action. Existing session history is scanned when the extension starts.
## Safety ## Safety
+11 -19
View File
@@ -4,13 +4,12 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import type { AssistantMessage } from "@earendil-works/pi-ai"; import type { AssistantMessage } from "@earendil-works/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Box, MouseRegion, Text, type TuiMouseEvent } from "@earendil-works/pi-tui"; import { Text } from "@earendil-works/pi-tui";
const TYPE = "bash-code-actions"; const TYPE = "bash-code-actions";
const LANGS = new Set(["bash", "sh", "shell", "zsh"]); const LANGS = new Set(["bash", "sh", "shell", "zsh"]);
type Block = { index: number; language: string; code: string }; type Block = { index: number; language: string; code: string };
type Data = { sourceEntryId: string; cwd: string; blocks: Block[] }; type Data = { sourceEntryId: string; cwd: string; blocks: Block[] };
let current: ExtensionContext | undefined;
function body(message: unknown): string { function body(message: unknown): string {
const parts = (message as AssistantMessage | undefined)?.content; const parts = (message as AssistantMessage | undefined)?.content;
@@ -74,20 +73,14 @@ async function action(kind: "copy" | "open" | "execute", block: Block, data: Dat
function preview(code: string): string { function preview(code: string): string {
const line = code.split("\n").find(s => s.trim())?.trim() || "(vuoto)"; return line.length > 65 ? `${line.slice(0, 62)}...` : line; const line = code.split("\n").find(s => s.trim())?.trim() || "(vuoto)"; return line.length > 65 ? `${line.slice(0, 62)}...` : line;
} }
function button(label: string, kind: "copy" | "open" | "execute", block: Block, data: Data, theme: any): MouseRegion { function render(data: Data, theme: any): Text {
return new MouseRegion(new Text(theme.fg(kind === "execute" ? "warning" : "accent", `[ ${label} ]`), 1, 0), (e: TuiMouseEvent) => { const count = data.blocks.length;
if (e.type !== "click" || e.button !== "left") return undefined; const lines = [
if (current) void action(kind, block, data, current); return { handled: true, render: false }; theme.fg("muted", `${count} blocco${count === 1 ? "" : "chi"} shell rilevat${count === 1 ? "o" : "i"}.`),
}); theme.fg("dim", "Usa /bash-blocks per copiare, aprire nel terminale o eseguire con conferma."),
} ...data.blocks.map(b => theme.fg("dim", `${b.index}. ${preview(b.code)}`)),
function render(data: Data, theme: any): Box { ];
const box = new Box(1, 0, s => theme.bg("customMessageBg", s)); return new Text(lines.join("\n"), 1, 0);
box.addChild(new Text(theme.fg("muted", "bash code actions"), 1, 0));
for (const b of data.blocks) {
box.addChild(new Text(theme.fg("dim", `${b.index}. ${preview(b.code)}`), 1, 0));
box.addChild(button("copia", "copy", b, data, theme)); box.addChild(button("terminale", "open", b, data, theme)); box.addChild(button("esegui", "execute", b, data, theme));
}
return box;
} }
function known(ctx: ExtensionContext): Data[] { function known(ctx: ExtensionContext): Data[] {
return ctx.sessionManager.getBranch().filter((e: any) => e.type === "custom" && e.customType === TYPE).map((e: any) => e.data).filter((d: any) => d?.blocks); return ctx.sessionManager.getBranch().filter((e: any) => e.type === "custom" && e.customType === TYPE).map((e: any) => e.data).filter((d: any) => d?.blocks);
@@ -102,10 +95,9 @@ async function recover(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
} }
export default function (pi: ExtensionAPI) { export default function (pi: ExtensionAPI) {
pi.registerEntryRenderer(TYPE, (entry, _opts, theme) => { const d = entry.data as Data | undefined; return d?.blocks?.length ? render(d, theme) : undefined; }); pi.registerEntryRenderer(TYPE, (entry, _opts, theme) => { const d = entry.data as Data | undefined; return d?.blocks?.length ? render(d, theme) : undefined; });
pi.on("session_start", async (_e, ctx) => { current = ctx; await recover(pi, ctx); }); pi.on("session_start", async (_e, ctx) => { await recover(pi, ctx); });
pi.on("session_shutdown", () => { current = undefined; });
pi.on("turn_end", (e, ctx) => { pi.on("turn_end", (e, ctx) => {
current = ctx; if (e.message.role !== "assistant") return; if (e.message.role !== "assistant") return;
const blocks = parse(body(e.message)); if (blocks.length) pi.appendEntry(TYPE, { sourceEntryId: e.messageEntryId, cwd: ctx.cwd, blocks } satisfies Data); const blocks = parse(body(e.message)); if (blocks.length) pi.appendEntry(TYPE, { sourceEntryId: e.messageEntryId, cwd: ctx.cwd, blocks } satisfies Data);
}); });
pi.registerCommand("bash-blocks", { description: "Gestisce i blocchi shell della sessione", handler: async (_args, ctx) => { pi.registerCommand("bash-blocks", { description: "Gestisce i blocchi shell della sessione", handler: async (_args, ctx) => {