char-slop/ai-dots

ai dotfiles

git clone https://git.t4t.associates/char-slop/ai-dots

Charlotte Sompi: switch to earendil-works importsb9b9000

main
4.0 KiB141 linesraw
1/**
2 * Tools Extension
3 *
4 * Provides a /tools command to enable/disable tools interactively.
5 * Tool selection persists across session reloads and respects branch navigation.
6 *
7 * Usage:
8 * 1. Copy this file to ~/.pi/agent/extensions/ or your project's .pi/extensions/
9 * 2. Use /tools to open the tool selector
10 */
11
12import type { ExtensionAPI, ExtensionContext, ToolInfo } from "@earendil-works/pi-coding-agent";
13import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
14import { Container, type SettingItem, SettingsList } from "@earendil-works/pi-tui";
15
16// State persisted to session
17interface ToolsState {
18  enabledTools: string[];
19}
20
21export default function toolsExtension(pi: ExtensionAPI) {
22  // Track enabled tools
23  let enabledTools: Set<string> = new Set();
24  let allTools: ToolInfo[] = [];
25
26  // Persist current state
27  function persistState() {
28    pi.appendEntry<ToolsState>("tools-config", {
29      enabledTools: Array.from(enabledTools),
30    });
31  }
32
33  // Apply current tool selection
34  function applyTools() {
35    pi.setActiveTools(Array.from(enabledTools));
36  }
37
38  // Find the last tools-config entry in the current branch
39  function restoreFromBranch(ctx: ExtensionContext) {
40    allTools = pi.getAllTools();
41
42    // Get entries in current branch only
43    const branchEntries = ctx.sessionManager.getBranch();
44    let savedTools: string[] | undefined;
45
46    for (const entry of branchEntries) {
47      if (entry.type === "custom" && entry.customType === "tools-config") {
48        const data = entry.data as ToolsState | undefined;
49        if (data?.enabledTools) {
50          savedTools = data.enabledTools;
51        }
52      }
53    }
54
55    if (savedTools) {
56      // Restore saved tool selection (filter to only tools that still exist)
57      const allToolNames = allTools.map((t) => t.name);
58      enabledTools = new Set(savedTools.filter((t: string) => allToolNames.includes(t)));
59      applyTools();
60    } else {
61      // No saved state - sync with currently active tools
62      enabledTools = new Set(pi.getActiveTools());
63    }
64  }
65
66  // Register /tools command
67  pi.registerCommand("tools", {
68    description: "Enable/disable tools",
69    handler: async (_args, ctx) => {
70      // Refresh tool list
71      allTools = pi.getAllTools();
72
73      await ctx.ui.custom((tui, theme, _kb, done) => {
74        // Build settings items for each tool
75        const items: SettingItem[] = allTools.map((tool) => ({
76          id: tool.name,
77          label: tool.name,
78          currentValue: enabledTools.has(tool.name) ? "enabled" : "disabled",
79          values: ["enabled", "disabled"],
80        }));
81
82        const container = new Container();
83        container.addChild(
84          new (class {
85            render(_width: number) {
86              return [theme.fg("accent", theme.bold("Tool Configuration")), ""];
87            }
88            invalidate() {}
89          })(),
90        );
91
92        const settingsList = new SettingsList(
93          items,
94          Math.min(items.length + 2, 15),
95          getSettingsListTheme(),
96          (id, newValue) => {
97            // Update enabled state and apply immediately
98            if (newValue === "enabled") {
99              enabledTools.add(id);
100            } else {
101              enabledTools.delete(id);
102            }
103            applyTools();
104            persistState();
105          },
106          () => {
107            // Close dialog
108            done(undefined);
109          },
110        );
111
112        container.addChild(settingsList);
113
114        const component = {
115          render(width: number) {
116            return container.render(width);
117          },
118          invalidate() {
119            container.invalidate();
120          },
121          handleInput(data: string) {
122            settingsList.handleInput?.(data);
123            tui.requestRender();
124          },
125        };
126
127        return component;
128      });
129    },
130  });
131
132  // Restore state on session start
133  pi.on("session_start", async (_event, ctx) => {
134    restoreFromBranch(ctx);
135  });
136
137  // Restore state when navigating the session tree
138  pi.on("session_tree", async (_event, ctx) => {
139    restoreFromBranch(ctx);
140  });
141}