Custom tools with session lifecycle, examples for hooks and tools

- Custom tools: TypeScript modules that extend pi with new tools
  - Custom TUI rendering via renderCall/renderResult
  - User interaction via pi.ui (select, confirm, input, notify)
  - Session lifecycle via onSession callback for state reconstruction
  - Examples: todo.ts, question.ts, hello.ts

- Hook examples: permission-gate, git-checkpoint, protected-paths

- Session lifecycle centralized in AgentSession
  - Works across all modes (interactive, print, RPC)
  - Unified session event for hooks (replaces session_start/session_switch)

- Box component added to pi-tui

- Examples bundled in npm and binary releases

Fixes #190
This commit is contained in:
Mario Zechner 2025-12-17 16:03:23 +01:00
parent 295f51b53f
commit e7097d911a
33 changed files with 1926 additions and 117 deletions

View file

@ -0,0 +1,96 @@
import type { Component } from "../tui.js";
import { applyBackgroundToLine, visibleWidth } from "../utils.js";
/**
* Box component - a container that applies padding and background to all children
*/
export class Box implements Component {
children: Component[] = [];
private paddingX: number;
private paddingY: number;
private bgFn?: (text: string) => string;
constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {
this.paddingX = paddingX;
this.paddingY = paddingY;
this.bgFn = bgFn;
}
addChild(component: Component): void {
this.children.push(component);
}
removeChild(component: Component): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
clear(): void {
this.children = [];
}
setBgFn(bgFn?: (text: string) => string): void {
this.bgFn = bgFn;
}
invalidate(): void {
for (const child of this.children) {
child.invalidate?.();
}
}
render(width: number): string[] {
if (this.children.length === 0) {
return [];
}
const contentWidth = Math.max(1, width - this.paddingX * 2);
const leftPad = " ".repeat(this.paddingX);
// Render all children
const childLines: string[] = [];
for (const child of this.children) {
const lines = child.render(contentWidth);
for (const line of lines) {
childLines.push(leftPad + line);
}
}
if (childLines.length === 0) {
return [];
}
// Apply background and padding
const result: string[] = [];
// Top padding
for (let i = 0; i < this.paddingY; i++) {
result.push(this.applyBg("", width));
}
// Content
for (const line of childLines) {
result.push(this.applyBg(line, width));
}
// Bottom padding
for (let i = 0; i < this.paddingY; i++) {
result.push(this.applyBg("", width));
}
return result;
}
private applyBg(line: string, width: number): string {
const visLen = visibleWidth(line);
const padNeeded = Math.max(0, width - visLen);
const padded = line + " ".repeat(padNeeded);
if (this.bgFn) {
return applyBackgroundToLine(padded, width, this.bgFn);
}
return padded;
}
}