improve peon commands
This commit is contained in:
+474
-186
@@ -4,25 +4,23 @@
|
|||||||
* Announces Pi events with customizable sound packs following the CESP standard.
|
* Announces Pi events with customizable sound packs following the CESP standard.
|
||||||
* Default pack: Warcraft Orc Peon
|
* Default pack: Warcraft Orc Peon
|
||||||
*
|
*
|
||||||
* Usage:
|
* Commands:
|
||||||
* /peon status - Show current pack and settings
|
* /peon:status - Show current pack and settings
|
||||||
* /peon list - List installed packs
|
* /peon:list - List installed packs
|
||||||
* /peon set <pack> - Switch to a different pack
|
* /peon:set - Interactively select a sound pack
|
||||||
* /peon volume <0-100> - Set master volume
|
* /peon:settings - Interactive settings editor
|
||||||
* /peon mute - Toggle global mute
|
* /peon:volume [0-100] - Set or adjust master volume
|
||||||
* /peon test <category> - Test a sound category
|
* /peon:mute - Toggle global mute
|
||||||
*
|
* /peon:toggle [category]- Toggle sound categories
|
||||||
* Categories: session.start, task.acknowledge, task.complete, task.error,
|
* /peon:test [category] - Test a sound category
|
||||||
* input.required, resource.limit
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { exec, execFile } from "node:child_process";
|
import { exec, execFile } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
// ============ CONFIGURATION ============
|
// ============ CONFIGURATION ============
|
||||||
const PACKS_DIR = path.join(process.env.HOME || "~", ".config/openpeon/packs");
|
const PACKS_DIR = path.join(process.env.HOME || "~", ".config/openpeon/packs");
|
||||||
@@ -33,26 +31,23 @@ const DEBOUNCE_MS = 500;
|
|||||||
|
|
||||||
// Get the remote host to SSH back to (the machine the user is physically on)
|
// Get the remote host to SSH back to (the machine the user is physically on)
|
||||||
function getRemoteHost(): string | null {
|
function getRemoteHost(): string | null {
|
||||||
// Try SSH_CONNECTION to get client's IP (format: client_ip client_port server_ip server_port)
|
|
||||||
const sshConn = process.env.SSH_CONNECTION || "";
|
const sshConn = process.env.SSH_CONNECTION || "";
|
||||||
const clientIP = sshConn.split(" ")[0];
|
const clientIP = sshConn.split(" ")[0];
|
||||||
if (clientIP && clientIP !== "::1" && clientIP !== "127.0.0.1") {
|
if (clientIP && clientIP !== "::1" && clientIP !== "127.0.0.1") {
|
||||||
return clientIP;
|
return clientIP;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to SSH_CLIENT (older format)
|
|
||||||
const sshClient = process.env.SSH_CLIENT || "";
|
const sshClient = process.env.SSH_CLIENT || "";
|
||||||
const clientIP2 = sshClient.split(" ")[0];
|
const clientIP2 = sshClient.split(" ")[0];
|
||||||
if (clientIP2 && clientIP2 !== "::1" && clientIP2 !== "127.0.0.1") {
|
if (clientIP2 && clientIP2 !== "::1" && clientIP2 !== "127.0.0.1") {
|
||||||
return clientIP2;
|
return clientIP2;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Mac paths to Linux paths for SSH playback
|
// Map Mac paths to Linux paths for SSH playback
|
||||||
function mapPathToRemote(soundPath: string): string {
|
function mapPathToRemote(soundPath: string): string {
|
||||||
// Mac home → Linux home (different username)
|
|
||||||
if (soundPath.startsWith("/Users/thomasglopes/")) {
|
if (soundPath.startsWith("/Users/thomasglopes/")) {
|
||||||
return soundPath.replace("/Users/thomasglopes/", "/home/thomasgl/");
|
return soundPath.replace("/Users/thomasglopes/", "/home/thomasgl/");
|
||||||
}
|
}
|
||||||
@@ -61,14 +56,9 @@ function mapPathToRemote(soundPath: string): string {
|
|||||||
|
|
||||||
// ============ SSH DETECTION ============
|
// ============ SSH DETECTION ============
|
||||||
function isSSH(): boolean {
|
function isSSH(): boolean {
|
||||||
if (
|
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) {
|
||||||
process.env.SSH_CONNECTION ||
|
|
||||||
process.env.SSH_CLIENT ||
|
|
||||||
process.env.SSH_TTY
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check for sshd-session process (works in tmux/zellij)
|
|
||||||
try {
|
try {
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const result = execSync("pgrep -u $USER -f sshd-session 2>/dev/null", {
|
const result = execSync("pgrep -u $USER -f sshd-session 2>/dev/null", {
|
||||||
@@ -80,7 +70,6 @@ function isSSH(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// =======================================
|
|
||||||
|
|
||||||
// CESP Core Categories
|
// CESP Core Categories
|
||||||
const CORE_CATEGORIES = [
|
const CORE_CATEGORIES = [
|
||||||
@@ -115,12 +104,11 @@ interface OpenPeonManifest {
|
|||||||
|
|
||||||
interface PeonConfig {
|
interface PeonConfig {
|
||||||
activePack: string;
|
activePack: string;
|
||||||
volume: number; // 0.0 to 1.0
|
volume: number;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
enabledCategories: Record<Category, boolean>;
|
enabledCategories: Record<Category, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default config
|
|
||||||
const DEFAULT_CONFIG: PeonConfig = {
|
const DEFAULT_CONFIG: PeonConfig = {
|
||||||
activePack: DEFAULT_PACK,
|
activePack: DEFAULT_PACK,
|
||||||
volume: 0.7,
|
volume: 0.7,
|
||||||
@@ -138,6 +126,8 @@ const DEFAULT_CONFIG: PeonConfig = {
|
|||||||
// State
|
// State
|
||||||
let config: PeonConfig = { ...DEFAULT_CONFIG };
|
let config: PeonConfig = { ...DEFAULT_CONFIG };
|
||||||
let lastPlayed: number = 0;
|
let lastPlayed: number = 0;
|
||||||
|
let lastSoundPerCategory: Map<Category, string> = new Map();
|
||||||
|
let manifestCache: Map<string, OpenPeonManifest> = new Map();
|
||||||
|
|
||||||
// ============ CONFIG PERSISTENCE ============
|
// ============ CONFIG PERSISTENCE ============
|
||||||
function loadConfig(): PeonConfig {
|
function loadConfig(): PeonConfig {
|
||||||
@@ -164,29 +154,22 @@ function saveConfig(): void {
|
|||||||
console.error("[peon] Failed to save config:", err);
|
console.error("[peon] Failed to save config:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let lastSoundPerCategory: Map<Category, string> = new Map();
|
|
||||||
let manifestCache: Map<string, OpenPeonManifest> = new Map();
|
|
||||||
|
|
||||||
// ============ AUDIO PLAYBACK ============
|
// ============ AUDIO PLAYBACK ============
|
||||||
|
|
||||||
function playSoundLocal(soundPath: string, volume: number): void {
|
function playSoundLocal(soundPath: string, volume: number): void {
|
||||||
if (volume <= 0) return;
|
if (volume <= 0) return;
|
||||||
|
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
|
|
||||||
if (platform === "darwin") {
|
if (platform === "darwin") {
|
||||||
// macOS
|
|
||||||
const vol = volume.toFixed(2);
|
const vol = volume.toFixed(2);
|
||||||
exec(`nohup afplay -v ${vol} "${soundPath}" >/dev/null 2>&1 &`);
|
exec(`nohup afplay -v ${vol} "${soundPath}" >/dev/null 2>&1 &`);
|
||||||
} else if (platform === "linux") {
|
} else if (platform === "linux") {
|
||||||
// Linux - try PipeWire first, then fall back
|
|
||||||
execFile("pw-play", ["--volume=" + volume, soundPath], (err) => {
|
execFile("pw-play", ["--volume=" + volume, soundPath], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
// Fallback to paplay (PulseAudio)
|
|
||||||
const paVol = Math.round(volume * 65536);
|
const paVol = Math.round(volume * 65536);
|
||||||
execFile("paplay", ["--volume=" + paVol, soundPath], (err2) => {
|
execFile("paplay", ["--volume=" + paVol, soundPath], (err2) => {
|
||||||
if (err2) {
|
if (err2) {
|
||||||
// Final fallback to aplay (ALSA)
|
|
||||||
execFile("aplay", [soundPath]);
|
execFile("aplay", [soundPath]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -197,37 +180,31 @@ function playSoundLocal(soundPath: string, volume: number): void {
|
|||||||
|
|
||||||
function playSound(soundPath: string, volume: number): void {
|
function playSound(soundPath: string, volume: number): void {
|
||||||
const remoteHost = getRemoteHost();
|
const remoteHost = getRemoteHost();
|
||||||
|
|
||||||
// If in SSH session from Mac, play only on the remote Linux machine (where user physically is)
|
|
||||||
if (remoteHost && isSSH() && process.platform === "darwin") {
|
if (remoteHost && isSSH() && process.platform === "darwin") {
|
||||||
const remotePath = mapPathToRemote(soundPath);
|
const remotePath = mapPathToRemote(soundPath);
|
||||||
// SSH back to Linux with correct username and mapped path
|
|
||||||
// pw-play expects volume as 0.0-1.0 float
|
|
||||||
exec(
|
exec(
|
||||||
`ssh -o ConnectTimeout=2 -o BatchMode=yes thomasgl@${remoteHost} "pw-play --volume=${volume} '${remotePath}' 2>/dev/null" 2>/dev/null || true`,
|
`ssh -o ConnectTimeout=2 -o BatchMode=yes thomasgl@${remoteHost} "pw-play --volume=${volume} '${remotePath}' 2>/dev/null" 2>/dev/null || true`,
|
||||||
{ timeout: 5000 }
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
return; // Don't play locally - user is on Linux
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If in SSH session from Linux, play only on remote Mac
|
|
||||||
if (remoteHost && isSSH() && process.platform === "linux") {
|
if (remoteHost && isSSH() && process.platform === "linux") {
|
||||||
const vol = Math.round(volume * 100);
|
const vol = Math.round(volume * 100);
|
||||||
exec(
|
exec(
|
||||||
`ssh -o ConnectTimeout=2 -o BatchMode=yes ${remoteHost} "afplay -v ${vol/100} '${soundPath}' 2>/dev/null" 2>/dev/null || true`,
|
`ssh -o ConnectTimeout=2 -o BatchMode=yes ${remoteHost} "afplay -v ${vol / 100} '${soundPath}' 2>/dev/null" 2>/dev/null || true`,
|
||||||
{ timeout: 5000 }
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
return; // Don't play locally - user is on Mac
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play locally when not in SSH session
|
|
||||||
playSoundLocal(soundPath, volume);
|
playSoundLocal(soundPath, volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendNotification(title: string, message: string): void {
|
function sendNotification(title: string, message: string): void {
|
||||||
const remoteHost = getRemoteHost();
|
const remoteHost = getRemoteHost();
|
||||||
|
|
||||||
// If SSH'd from Mac to Linux, send notification to Linux (where user is)
|
|
||||||
if (remoteHost && isSSH() && process.platform === "darwin") {
|
if (remoteHost && isSSH() && process.platform === "darwin") {
|
||||||
exec(
|
exec(
|
||||||
`ssh -o ConnectTimeout=2 -o BatchMode=yes thomasgl@${remoteHost} "notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg '${title}' '${message}'" 2>/dev/null || true`,
|
`ssh -o ConnectTimeout=2 -o BatchMode=yes thomasgl@${remoteHost} "notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg '${title}' '${message}'" 2>/dev/null || true`,
|
||||||
@@ -235,17 +212,15 @@ function sendNotification(title: string, message: string): void {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If SSH'd from Linux to Mac, send notification to Mac (where user is)
|
|
||||||
if (remoteHost && isSSH() && process.platform === "linux") {
|
if (remoteHost && isSSH() && process.platform === "linux") {
|
||||||
exec(
|
exec(
|
||||||
`ssh -o ConnectTimeout=2 -o BatchMode=yes ${remoteHost} "osascript -e 'display notification \"${message}\" with title \"${title}\"'" 2>/dev/null || true`,
|
`ssh -o ConnectTimeout=2 -o BatchMode=yes ${remoteHost} "osascript -e 'display notification "${message}" with title "${title}"'" 2>/dev/null || true`,
|
||||||
{ timeout: 5000 }
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send locally when not in SSH session
|
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
exec(
|
exec(
|
||||||
`notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg '${title}' '${message}' 2>/dev/null || true`,
|
`notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg '${title}' '${message}' 2>/dev/null || true`,
|
||||||
@@ -260,7 +235,6 @@ function sendNotification(title: string, message: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============ PACK MANAGEMENT ============
|
// ============ PACK MANAGEMENT ============
|
||||||
|
|
||||||
function getPackPath(packName: string): string {
|
function getPackPath(packName: string): string {
|
||||||
return path.join(PACKS_DIR, packName);
|
return path.join(PACKS_DIR, packName);
|
||||||
}
|
}
|
||||||
@@ -297,13 +271,16 @@ function getInstalledPacks(): string[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPackDisplayName(packName: string): string {
|
||||||
|
const manifest = loadManifest(packName);
|
||||||
|
return manifest?.display_name || packName;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveCategory(manifest: OpenPeonManifest, category: Category): CategoryConfig | null {
|
function resolveCategory(manifest: OpenPeonManifest, category: Category): CategoryConfig | null {
|
||||||
// Direct lookup
|
|
||||||
if (manifest.categories[category]) {
|
if (manifest.categories[category]) {
|
||||||
return manifest.categories[category];
|
return manifest.categories[category];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check aliases
|
|
||||||
if (manifest.category_aliases) {
|
if (manifest.category_aliases) {
|
||||||
const aliased = manifest.category_aliases[category];
|
const aliased = manifest.category_aliases[category];
|
||||||
if (aliased && manifest.categories[aliased]) {
|
if (aliased && manifest.categories[aliased]) {
|
||||||
@@ -318,53 +295,44 @@ function pickSound(categoryConfig: CategoryConfig, category: Category): Sound |
|
|||||||
const sounds = categoryConfig.sounds;
|
const sounds = categoryConfig.sounds;
|
||||||
if (sounds.length === 0) return null;
|
if (sounds.length === 0) return null;
|
||||||
|
|
||||||
// No-repeat: exclude last played sound if there are alternatives
|
|
||||||
const lastSound = lastSoundPerCategory.get(category);
|
const lastSound = lastSoundPerCategory.get(category);
|
||||||
let candidates = sounds;
|
let candidates = sounds;
|
||||||
if (lastSound && sounds.length > 1) {
|
if (lastSound && sounds.length > 1) {
|
||||||
candidates = sounds.filter((s) => s.file !== lastSound);
|
candidates = sounds.filter((s) => s.file !== lastSound);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random selection
|
|
||||||
const sound = candidates[Math.floor(Math.random() * candidates.length)];
|
const sound = candidates[Math.floor(Math.random() * candidates.length)];
|
||||||
lastSoundPerCategory.set(category, sound.file);
|
lastSoundPerCategory.set(category, sound.file);
|
||||||
return sound;
|
return sound;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ SOUND PLAYBACK ============
|
// ============ SOUND PLAYBACK ============
|
||||||
|
|
||||||
function play(category: Category): void {
|
function play(category: Category): void {
|
||||||
if (config.muted) return;
|
if (config.muted) return;
|
||||||
if (!config.enabledCategories[category]) return;
|
if (!config.enabledCategories[category]) return;
|
||||||
|
|
||||||
// Global debounce check - never play two sounds at once
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastPlayed < DEBOUNCE_MS) {
|
if (now - lastPlayed < DEBOUNCE_MS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastPlayed = now;
|
lastPlayed = now;
|
||||||
|
|
||||||
// Load manifest
|
|
||||||
const manifest = loadManifest(config.activePack);
|
const manifest = loadManifest(config.activePack);
|
||||||
if (!manifest) {
|
if (!manifest) {
|
||||||
console.error(`[peon] Pack not found: ${config.activePack}`);
|
console.error(`[peon] Pack not found: ${config.activePack}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve category
|
|
||||||
const categoryConfig = resolveCategory(manifest, category);
|
const categoryConfig = resolveCategory(manifest, category);
|
||||||
if (!categoryConfig) {
|
if (!categoryConfig) {
|
||||||
// Silently skip if pack doesn't have this category
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick and play sound
|
|
||||||
const sound = pickSound(categoryConfig, category);
|
const sound = pickSound(categoryConfig, category);
|
||||||
if (!sound) return;
|
if (!sound) return;
|
||||||
|
|
||||||
const soundPath = path.join(getPackPath(config.activePack), sound.file);
|
const soundPath = path.join(getPackPath(config.activePack), sound.file);
|
||||||
if (!fs.existsSync(soundPath)) {
|
if (!fs.existsSync(soundPath)) {
|
||||||
// Try with sounds/ prefix if no slash in path
|
|
||||||
if (!sound.file.includes("/")) {
|
if (!sound.file.includes("/")) {
|
||||||
const altPath = path.join(getPackPath(config.activePack), "sounds", sound.file);
|
const altPath = path.join(getPackPath(config.activePack), "sounds", sound.file);
|
||||||
if (fs.existsSync(altPath)) {
|
if (fs.existsSync(altPath)) {
|
||||||
@@ -378,10 +346,9 @@ function play(category: Category): void {
|
|||||||
|
|
||||||
playSound(soundPath, config.volume);
|
playSound(soundPath, config.volume);
|
||||||
|
|
||||||
// Send desktop notification for important events
|
|
||||||
const notificationMessages: Record<Category, { title: string; message: string } | null> = {
|
const notificationMessages: Record<Category, { title: string; message: string } | null> = {
|
||||||
"session.start": null, // Too noisy on startup
|
"session.start": null,
|
||||||
"task.acknowledge": null, // Too noisy
|
"task.acknowledge": null,
|
||||||
"task.complete": { title: "Pi", message: "Task complete" },
|
"task.complete": { title: "Pi", message: "Task complete" },
|
||||||
"task.error": { title: "Pi", message: "Task failed" },
|
"task.error": { title: "Pi", message: "Task failed" },
|
||||||
"input.required": { title: "Pi", message: "Input required" },
|
"input.required": { title: "Pi", message: "Input required" },
|
||||||
@@ -397,150 +364,475 @@ function play(category: Category): void {
|
|||||||
// ============ COMMANDS ============
|
// ============ COMMANDS ============
|
||||||
|
|
||||||
function registerCommands(pi: ExtensionAPI) {
|
function registerCommands(pi: ExtensionAPI) {
|
||||||
pi.registerCommand("peon", {
|
// /peon:status - Show current pack and settings
|
||||||
description: "Manage sound packs and settings",
|
pi.registerCommand("peon:status", {
|
||||||
handler: async (args: string, ctx: ExtensionContext) => {
|
description: "Show current sound pack and settings",
|
||||||
const parts = args.trim().split(/\s+/);
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
||||||
const cmd = parts[0] || "status";
|
const manifest = loadManifest(config.activePack);
|
||||||
|
const packDisplay = manifest?.display_name || config.activePack;
|
||||||
|
const lines = [
|
||||||
|
`Active pack: ${packDisplay}`,
|
||||||
|
`Volume: ${Math.round(config.volume * 100)}%`,
|
||||||
|
`Muted: ${config.muted ? "yes" : "no"}`,
|
||||||
|
"",
|
||||||
|
"Enabled categories:",
|
||||||
|
];
|
||||||
|
for (const cat of CORE_CATEGORIES) {
|
||||||
|
const enabled = config.enabledCategories[cat] ? "✓" : "✗";
|
||||||
|
lines.push(` ${enabled} ${cat}`);
|
||||||
|
}
|
||||||
|
ctx.ui.notify(lines.join("\n"), "info");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
switch (cmd) {
|
// /peon:list - List installed packs
|
||||||
case "status": {
|
pi.registerCommand("peon:list", {
|
||||||
const manifest = loadManifest(config.activePack);
|
description: "List installed sound packs",
|
||||||
const packDisplay = manifest?.display_name || config.activePack;
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
||||||
const lines = [
|
const packs = getInstalledPacks();
|
||||||
`Active pack: ${packDisplay}`,
|
if (packs.length === 0) {
|
||||||
`Volume: ${Math.round(config.volume * 100)}%`,
|
ctx.ui.notify("No packs installed. Add packs to ~/.config/openpeon/packs/", "warning");
|
||||||
`Muted: ${config.muted}`,
|
} else {
|
||||||
"Enabled categories:",
|
const lines = ["Installed packs:"];
|
||||||
];
|
for (const pack of packs) {
|
||||||
for (const cat of CORE_CATEGORIES) {
|
const m = loadManifest(pack);
|
||||||
const enabled = config.enabledCategories[cat] ? "✓" : "✗";
|
const marker = pack === config.activePack ? "→ " : " ";
|
||||||
lines.push(` ${enabled} ${cat}`);
|
lines.push(`${marker}${m?.display_name || pack} (${pack})`);
|
||||||
}
|
|
||||||
ctx.ui.notify(lines.join("\n"), "info");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "list": {
|
|
||||||
const packs = getInstalledPacks();
|
|
||||||
if (packs.length === 0) {
|
|
||||||
ctx.ui.notify("No packs installed. Add packs to ~/.config/openpeon/packs/", "warning");
|
|
||||||
} else {
|
|
||||||
const lines = ["Installed packs:"];
|
|
||||||
for (const pack of packs) {
|
|
||||||
const m = loadManifest(pack);
|
|
||||||
const marker = pack === config.activePack ? "→ " : " ";
|
|
||||||
lines.push(`${marker}${m?.display_name || pack} (${pack})`);
|
|
||||||
}
|
|
||||||
ctx.ui.notify(lines.join("\n"), "info");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "set": {
|
|
||||||
const packName = parts[1];
|
|
||||||
if (!packName) {
|
|
||||||
ctx.ui.notify("Usage: /peon set <pack-name>", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const packs = getInstalledPacks();
|
|
||||||
if (!packs.includes(packName)) {
|
|
||||||
ctx.ui.notify(`Pack '${packName}' not found. Run /peon list to see installed packs.`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
config.activePack = packName;
|
|
||||||
manifestCache.delete(packName); // Clear cache
|
|
||||||
saveConfig();
|
|
||||||
ctx.ui.notify(`Switched to pack: ${packName}`, "info");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "volume": {
|
|
||||||
const volStr = parts[1];
|
|
||||||
if (!volStr) {
|
|
||||||
ctx.ui.notify(`Current volume: ${Math.round(config.volume * 100)}%`, "info");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const vol = parseInt(volStr, 10);
|
|
||||||
if (isNaN(vol) || vol < 0 || vol > 100) {
|
|
||||||
ctx.ui.notify("Volume must be 0-100", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
config.volume = vol / 100;
|
|
||||||
saveConfig();
|
|
||||||
ctx.ui.notify(`Volume set to ${vol}%`, "info");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mute": {
|
|
||||||
config.muted = !config.muted;
|
|
||||||
saveConfig();
|
|
||||||
ctx.ui.notify(config.muted ? "Muted" : "Unmuted", "info");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "toggle": {
|
|
||||||
const cat = parts[1] as Category;
|
|
||||||
if (!CORE_CATEGORIES.includes(cat)) {
|
|
||||||
ctx.ui.notify(`Unknown category: ${cat}\nAvailable: ${CORE_CATEGORIES.join(", ")}`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
config.enabledCategories[cat] = !config.enabledCategories[cat];
|
|
||||||
saveConfig();
|
|
||||||
ctx.ui.notify(`${cat}: ${config.enabledCategories[cat] ? "enabled" : "disabled"}`, "info");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "test": {
|
|
||||||
const cat = parts[1] as Category;
|
|
||||||
if (!CORE_CATEGORIES.includes(cat)) {
|
|
||||||
ctx.ui.notify(`Unknown category: ${cat}\nAvailable: ${CORE_CATEGORIES.join(", ")}`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.ui.notify(`Testing ${cat}...`, "info");
|
|
||||||
play(cat);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
ctx.ui.notify(
|
|
||||||
"Usage: /peon [status|list|set <pack>|volume <0-100>|mute|toggle <category>|test <category>]",
|
|
||||||
"info"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
ctx.ui.notify(lines.join("\n"), "info");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// /peon:set - Interactively select a sound pack
|
||||||
|
pi.registerCommand("peon:set", {
|
||||||
|
description: "Select a sound pack interactively",
|
||||||
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
||||||
|
await handleSetCommand(args.trim(), ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// /peon:settings - Interactive settings editor
|
||||||
|
pi.registerCommand("peon:settings", {
|
||||||
|
description: "Edit peon settings interactively",
|
||||||
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
||||||
|
await showSettingsMenu(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// /peon:volume - Set or adjust volume
|
||||||
|
pi.registerCommand("peon:volume", {
|
||||||
|
description: "Set master volume (0-100) or adjust interactively",
|
||||||
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
||||||
|
await handleVolumeCommand(args.trim(), ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// /peon:mute - Toggle mute
|
||||||
|
pi.registerCommand("peon:mute", {
|
||||||
|
description: "Toggle global mute",
|
||||||
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
||||||
|
config.muted = !config.muted;
|
||||||
|
saveConfig();
|
||||||
|
ctx.ui.notify(config.muted ? "Muted" : "Unmuted", "info");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// /peon:toggle - Toggle sound categories
|
||||||
|
pi.registerCommand("peon:toggle", {
|
||||||
|
description: "Toggle sound categories on/off",
|
||||||
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
||||||
|
await handleToggleCommand(args.trim(), ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// /peon:test - Test sound categories
|
||||||
|
pi.registerCommand("peon:test", {
|
||||||
|
description: "Test a sound category",
|
||||||
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
||||||
|
await handleTestCommand(args.trim(), ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ COMMAND HANDLERS ============
|
||||||
|
|
||||||
|
async function handleSetCommand(packName: string, ctx: ExtensionCommandContext) {
|
||||||
|
const packs = getInstalledPacks();
|
||||||
|
|
||||||
|
if (packs.length === 0) {
|
||||||
|
ctx.ui.notify("No packs installed. Add packs to ~/.config/openpeon/packs/", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pack name provided directly, use it
|
||||||
|
if (packName) {
|
||||||
|
if (!packs.includes(packName)) {
|
||||||
|
ctx.ui.notify(`Pack '${packName}' not found.`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.activePack = packName;
|
||||||
|
manifestCache.delete(packName);
|
||||||
|
saveConfig();
|
||||||
|
ctx.ui.notify(`Switched to pack: ${getPackDisplayName(packName)}`, "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive selection using SelectList
|
||||||
|
const items: SelectItem[] = packs.map((pack) => ({
|
||||||
|
value: pack,
|
||||||
|
label: getPackDisplayName(pack),
|
||||||
|
description: pack === config.activePack ? "currently active" : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
||||||
|
const container = new Container();
|
||||||
|
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
container.addChild(new Text(theme.fg("accent", theme.bold("Select Sound Pack")), 1, 0));
|
||||||
|
|
||||||
|
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
||||||
|
selectedPrefix: (t) => theme.fg("accent", t),
|
||||||
|
selectedText: (t) => theme.fg("accent", t),
|
||||||
|
description: (t) => theme.fg("muted", t),
|
||||||
|
scrollInfo: (t) => theme.fg("dim", t),
|
||||||
|
noMatch: (t) => theme.fg("warning", t),
|
||||||
|
});
|
||||||
|
selectList.onSelect = (item) => done(item.value);
|
||||||
|
selectList.onCancel = () => done(null);
|
||||||
|
container.addChild(selectList);
|
||||||
|
|
||||||
|
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: (w) => container.render(w),
|
||||||
|
invalidate: () => container.invalidate(),
|
||||||
|
handleInput: (data) => {
|
||||||
|
selectList.handleInput(data);
|
||||||
|
tui.requestRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
config.activePack = result;
|
||||||
|
manifestCache.delete(result);
|
||||||
|
saveConfig();
|
||||||
|
ctx.ui.notify(`Switched to pack: ${getPackDisplayName(result)}`, "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVolumeCommand(volStr: string, ctx: ExtensionCommandContext) {
|
||||||
|
if (!volStr) {
|
||||||
|
// Interactive volume selection
|
||||||
|
const volumes = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
||||||
|
const currentVol = Math.round(config.volume * 100);
|
||||||
|
|
||||||
|
const items: SelectItem[] = volumes.map((v) => ({
|
||||||
|
value: String(v),
|
||||||
|
label: `${v}%`,
|
||||||
|
description: v === currentVol ? "current" : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
||||||
|
const container = new Container();
|
||||||
|
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
container.addChild(new Text(theme.fg("accent", theme.bold("Select Volume")), 1, 0));
|
||||||
|
|
||||||
|
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
||||||
|
selectedPrefix: (t) => theme.fg("accent", t),
|
||||||
|
selectedText: (t) => theme.fg("accent", t),
|
||||||
|
description: (t) => theme.fg("muted", t),
|
||||||
|
scrollInfo: (t) => theme.fg("dim", t),
|
||||||
|
noMatch: (t) => theme.fg("warning", t),
|
||||||
|
});
|
||||||
|
selectList.onSelect = (item) => done(item.value);
|
||||||
|
selectList.onCancel = () => done(null);
|
||||||
|
container.addChild(selectList);
|
||||||
|
|
||||||
|
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: (w) => container.render(w),
|
||||||
|
invalidate: () => container.invalidate(),
|
||||||
|
handleInput: (data) => {
|
||||||
|
selectList.handleInput(data);
|
||||||
|
tui.requestRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const vol = parseInt(result, 10);
|
||||||
|
config.volume = vol / 100;
|
||||||
|
saveConfig();
|
||||||
|
ctx.ui.notify(`Volume set to ${vol}%`, "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vol = parseInt(volStr, 10);
|
||||||
|
if (isNaN(vol) || vol < 0 || vol > 100) {
|
||||||
|
ctx.ui.notify("Volume must be 0-100", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.volume = vol / 100;
|
||||||
|
saveConfig();
|
||||||
|
ctx.ui.notify(`Volume set to ${vol}%`, "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showSettingsMenu(ctx: ExtensionCommandContext) {
|
||||||
|
type SettingsAction = "volume" | "mute" | "pack" | "categories" | "test" | "done";
|
||||||
|
|
||||||
|
const result = await ctx.ui.custom<SettingsAction | null>((tui, theme, _kb, done) => {
|
||||||
|
const items: SelectItem[] = [
|
||||||
|
{ value: "volume", label: "Volume", description: `${Math.round(config.volume * 100)}%` },
|
||||||
|
{ value: "mute", label: "Muted", description: config.muted ? "yes" : "no" },
|
||||||
|
{ value: "pack", label: "Active Pack", description: getPackDisplayName(config.activePack) },
|
||||||
|
{ value: "categories", label: "Toggle Categories..." },
|
||||||
|
{ value: "test", label: "Test Sounds..." },
|
||||||
|
{ value: "done", label: "Done" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const container = new Container();
|
||||||
|
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
container.addChild(new Text(theme.fg("accent", theme.bold("Peon Settings")), 1, 0));
|
||||||
|
|
||||||
|
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
||||||
|
selectedPrefix: (t) => theme.fg("accent", t),
|
||||||
|
selectedText: (t) => theme.fg("accent", t),
|
||||||
|
description: (t) => theme.fg("muted", t),
|
||||||
|
scrollInfo: (t) => theme.fg("dim", t),
|
||||||
|
noMatch: (t) => theme.fg("warning", t),
|
||||||
|
});
|
||||||
|
selectList.onSelect = (item) => done(item.value as SettingsAction);
|
||||||
|
selectList.onCancel = () => done(null);
|
||||||
|
container.addChild(selectList);
|
||||||
|
|
||||||
|
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: (w) => container.render(w),
|
||||||
|
invalidate: () => container.invalidate(),
|
||||||
|
handleInput: (data) => {
|
||||||
|
selectList.handleInput(data);
|
||||||
|
tui.requestRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result || result === "done") {
|
||||||
|
if (result === "done") {
|
||||||
|
ctx.ui.notify("Settings saved", "info");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle submenu
|
||||||
|
switch (result) {
|
||||||
|
case "volume":
|
||||||
|
await handleVolumeCommand("", ctx);
|
||||||
|
break;
|
||||||
|
case "mute":
|
||||||
|
config.muted = !config.muted;
|
||||||
|
saveConfig();
|
||||||
|
ctx.ui.notify(config.muted ? "Muted" : "Unmuted", "info");
|
||||||
|
break;
|
||||||
|
case "pack":
|
||||||
|
await handleSetCommand("", ctx);
|
||||||
|
break;
|
||||||
|
case "categories":
|
||||||
|
await showCategoryToggle(ctx);
|
||||||
|
break;
|
||||||
|
case "test":
|
||||||
|
await handleTestCommand("", ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return to settings menu
|
||||||
|
await showSettingsMenu(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showCategoryToggle(ctx: ExtensionCommandContext) {
|
||||||
|
const result = await ctx.ui.custom<Category | "done" | null>((tui, theme, _kb, done) => {
|
||||||
|
const items: SelectItem[] = [
|
||||||
|
...CORE_CATEGORIES.map((cat) => ({
|
||||||
|
value: cat,
|
||||||
|
label: `${config.enabledCategories[cat] ? "✓" : "✗"} ${cat}`,
|
||||||
|
})),
|
||||||
|
{ value: "done", label: "← Back" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const container = new Container();
|
||||||
|
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
container.addChild(new Text(theme.fg("accent", theme.bold("Toggle Categories (Space to toggle)")), 1, 0));
|
||||||
|
|
||||||
|
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
||||||
|
selectedPrefix: (t) => theme.fg("accent", t),
|
||||||
|
selectedText: (t) => theme.fg("accent", t),
|
||||||
|
description: (t) => theme.fg("muted", t),
|
||||||
|
scrollInfo: (t) => theme.fg("dim", t),
|
||||||
|
noMatch: (t) => theme.fg("warning", t),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle selection to toggle
|
||||||
|
selectList.onSelect = (item) => {
|
||||||
|
if (item.value === "done") {
|
||||||
|
done("done");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Toggle the category
|
||||||
|
const cat = item.value as Category;
|
||||||
|
config.enabledCategories[cat] = !config.enabledCategories[cat];
|
||||||
|
saveConfig();
|
||||||
|
// Update the label
|
||||||
|
item.label = `${config.enabledCategories[cat] ? "✓" : "✗"} ${cat}`;
|
||||||
|
tui.requestRender();
|
||||||
|
};
|
||||||
|
|
||||||
|
selectList.onCancel = () => done(null);
|
||||||
|
container.addChild(selectList);
|
||||||
|
|
||||||
|
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter toggle • esc back"), 1, 0));
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: (w) => container.render(w),
|
||||||
|
invalidate: () => container.invalidate(),
|
||||||
|
handleInput: (data) => {
|
||||||
|
selectList.handleInput(data);
|
||||||
|
tui.requestRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) return;
|
||||||
|
if (result === "done") return;
|
||||||
|
|
||||||
|
// If a category was selected, stay in the toggle menu
|
||||||
|
await showCategoryToggle(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleCommand(cat: string, ctx: ExtensionCommandContext) {
|
||||||
|
if (!cat) {
|
||||||
|
await showCategoryToggle(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CORE_CATEGORIES.includes(cat as Category)) {
|
||||||
|
ctx.ui.notify(`Unknown category: ${cat}\nAvailable: ${CORE_CATEGORIES.join(", ")}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.enabledCategories[cat as Category] = !config.enabledCategories[cat as Category];
|
||||||
|
saveConfig();
|
||||||
|
ctx.ui.notify(`${cat}: ${config.enabledCategories[cat as Category] ? "enabled" : "disabled"}`, "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTestCommand(cat: string, ctx: ExtensionCommandContext) {
|
||||||
|
if (!cat) {
|
||||||
|
// Interactive test selection - stays open after testing
|
||||||
|
await showTestMenu(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CORE_CATEGORIES.includes(cat as Category)) {
|
||||||
|
ctx.ui.notify(`Unknown category: ${cat}\nAvailable: ${CORE_CATEGORIES.join(", ")}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ui.notify(`Testing ${cat}...`, "info");
|
||||||
|
play(cat as Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showTestMenu(ctx: ExtensionCommandContext) {
|
||||||
|
const result = await ctx.ui.custom<Category | "done" | null>((tui, theme, _kb, done) => {
|
||||||
|
const items: SelectItem[] = [
|
||||||
|
...CORE_CATEGORIES.map((category) => ({
|
||||||
|
value: category,
|
||||||
|
label: category,
|
||||||
|
})),
|
||||||
|
{ value: "done", label: "← Back" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const container = new Container();
|
||||||
|
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
container.addChild(new Text(theme.fg("accent", theme.bold("Test Sound Category")), 1, 0));
|
||||||
|
|
||||||
|
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
||||||
|
selectedPrefix: (t) => theme.fg("accent", t),
|
||||||
|
selectedText: (t) => theme.fg("accent", t),
|
||||||
|
description: (t) => theme.fg("muted", t),
|
||||||
|
scrollInfo: (t) => theme.fg("dim", t),
|
||||||
|
noMatch: (t) => theme.fg("warning", t),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Play sound but stay open
|
||||||
|
selectList.onSelect = (item) => {
|
||||||
|
if (item.value === "done") {
|
||||||
|
done("done");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Play the sound but don't close
|
||||||
|
const category = item.value as Category;
|
||||||
|
ctx.ui.notify(`Testing ${category}...`, "info");
|
||||||
|
play(category);
|
||||||
|
};
|
||||||
|
|
||||||
|
selectList.onCancel = () => done(null);
|
||||||
|
container.addChild(selectList);
|
||||||
|
|
||||||
|
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter test • esc back"), 1, 0));
|
||||||
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: (w) => container.render(w),
|
||||||
|
invalidate: () => container.invalidate(),
|
||||||
|
handleInput: (data) => {
|
||||||
|
selectList.handleInput(data);
|
||||||
|
tui.requestRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null) return;
|
||||||
|
if (result === "done") return;
|
||||||
|
|
||||||
|
// If a category was tested, stay in the test menu
|
||||||
|
await showTestMenu(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ EVENT WIRING ============
|
// ============ EVENT WIRING ============
|
||||||
|
|
||||||
const INTERACTIVE_TOOLS = new Set(["question", "questionnaire"]);
|
const INTERACTIVE_TOOLS = new Set(["question", "questionnaire"]);
|
||||||
|
|
||||||
export default function(pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
registerCommands(pi);
|
registerCommands(pi);
|
||||||
|
|
||||||
// Session start
|
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
// Load persisted config
|
|
||||||
config = loadConfig();
|
config = loadConfig();
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
play("session.start");
|
play("session.start");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Task acknowledge - when agent starts working
|
|
||||||
pi.on("agent_start", async (_event, ctx) => {
|
pi.on("agent_start", async (_event, ctx) => {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
play("task.acknowledge");
|
play("task.acknowledge");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Task complete - when agent finishes
|
|
||||||
pi.on("agent_end", async (_event, ctx) => {
|
pi.on("agent_end", async (_event, ctx) => {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
play("task.complete");
|
play("task.complete");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Task error - when a tool errors
|
|
||||||
pi.on("tool_result", async (event, ctx) => {
|
pi.on("tool_result", async (event, ctx) => {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
if (event.isError) {
|
if (event.isError) {
|
||||||
@@ -548,7 +840,6 @@ export default function(pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Input required - when question/questionnaire is called
|
|
||||||
pi.on("tool_call", async (event, ctx) => {
|
pi.on("tool_call", async (event, ctx) => {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
if (INTERACTIVE_TOOLS.has(event.toolName)) {
|
if (INTERACTIVE_TOOLS.has(event.toolName)) {
|
||||||
@@ -556,10 +847,8 @@ export default function(pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resource limit - detect rate limiting in tool results
|
|
||||||
pi.on("tool_result", async (event, ctx) => {
|
pi.on("tool_result", async (event, ctx) => {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
// Check for rate limit indicators in error messages
|
|
||||||
const firstContent = event.content?.[0];
|
const firstContent = event.content?.[0];
|
||||||
const content = firstContent?.type === "text" ? firstContent.text : "";
|
const content = firstContent?.type === "text" ? firstContent.text : "";
|
||||||
if (event.isError && /rate.limit|quota|too.many.requests|429/i.test(content)) {
|
if (event.isError && /rate.limit|quota|too.many.requests|429/i.test(content)) {
|
||||||
@@ -567,7 +856,6 @@ export default function(pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for input_required events from other extensions (permission-gate, confirm-destructive, etc.)
|
|
||||||
pi.events.on("peon:input_required", (_data) => {
|
pi.events.on("peon:input_required", (_data) => {
|
||||||
play("input.required");
|
play("input.required");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user