pi-compact-tools 0.5.0 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
+ import { DEFAULT_CONFIG } from "./compact-tools-config.ts";
3
+ import type {
4
+ BuiltInDefinition,
5
+ CompactToolName,
6
+ CompactToolsConfig,
7
+ RenderContext,
8
+ RowState,
9
+ } from "./compact-tools-types.ts";
10
+
11
+ const MAX_TRACKED_ROWS = 2_000;
12
+ const SPINNING_ROWS_KEY = Symbol.for("pi.compact-tools.spinning-rows");
13
+ const EXECUTION_TIMINGS_KEY = Symbol.for("pi.compact-tools.execution-timings");
14
+
15
+ type ExecutionTiming = { startedAt: number; endedAt?: number };
16
+ type TrackedRow = { state: RowState; name: CompactToolName; invalidate: () => void };
17
+ type TimedExecute = (...args: any[]) => Promise<AgentToolResult<unknown>>;
18
+ type SharedState = typeof globalThis & {
19
+ [SPINNING_ROWS_KEY]?: Set<RowState>;
20
+ [EXECUTION_TIMINGS_KEY]?: Map<string, ExecutionTiming>;
21
+ };
22
+
23
+ export class ToolRuntime {
24
+ private configValue = DEFAULT_CONFIG;
25
+ private configRevision: object = {};
26
+ private animateRows = false;
27
+ private readonly spinningRows = new Set<RowState>();
28
+ private readonly executionTimings: Map<string, ExecutionTiming>;
29
+ private readonly trackedRows = new Map<string, TrackedRow>();
30
+
31
+ constructor() {
32
+ const shared = globalThis as SharedState;
33
+ for (const state of shared[SPINNING_ROWS_KEY] ?? []) {
34
+ if (state.timer) clearInterval(state.timer);
35
+ }
36
+ this.executionTimings = shared[EXECUTION_TIMINGS_KEY] ?? new Map<string, ExecutionTiming>();
37
+ shared[SPINNING_ROWS_KEY] = this.spinningRows;
38
+ shared[EXECUTION_TIMINGS_KEY] = this.executionTimings;
39
+ }
40
+
41
+ get config(): CompactToolsConfig {
42
+ return this.configValue;
43
+ }
44
+
45
+ get animatesRows(): boolean {
46
+ return this.animateRows;
47
+ }
48
+
49
+ configure(config: CompactToolsConfig, animateRows: boolean): void {
50
+ this.reset(false);
51
+ this.configValue = config;
52
+ this.animateRows = animateRows;
53
+ this.configRevision = {};
54
+ }
55
+
56
+ clearTimings(): void {
57
+ this.executionTimings.clear();
58
+ }
59
+
60
+ reset(clearTimings: boolean): void {
61
+ for (const state of this.spinningRows) this.stopSpinner(state);
62
+ this.trackedRows.clear();
63
+ if (clearTimings) this.clearTimings();
64
+ }
65
+
66
+ track(ctx: RenderContext, name: CompactToolName, state: RowState): void {
67
+ this.trackedRows.delete(ctx.toolCallId);
68
+ this.trackedRows.set(ctx.toolCallId, { state, name, invalidate: ctx.invalidate });
69
+ if (this.trackedRows.size <= MAX_TRACKED_ROWS) return;
70
+ const oldest = this.trackedRows.keys().next().value;
71
+ if (oldest !== undefined) this.trackedRows.delete(oldest);
72
+ }
73
+
74
+ toggleTrackedRows(): "expanded" | "collapsed" | undefined {
75
+ const rows: TrackedRow[] = [];
76
+ for (const row of this.trackedRows.values()) {
77
+ if (this.configValue.auto_compact[row.name] && row.state.hasResult) rows.push(row);
78
+ }
79
+ if (rows.length === 0) return undefined;
80
+ const expand = rows.some(({ state }) => !state.expanded);
81
+ for (const { state, invalidate } of rows) {
82
+ state.expanded = expand;
83
+ invalidate();
84
+ }
85
+ return expand ? "expanded" : "collapsed";
86
+ }
87
+
88
+ syncExpansion(state: RowState, hostExpanded: boolean, name: CompactToolName): boolean {
89
+ const initialized = this.initializeExpansion(state, name);
90
+ if (initialized || state.lastHostExpanded === undefined) {
91
+ state.lastHostExpanded = hostExpanded;
92
+ } else if (state.lastHostExpanded !== hostExpanded) {
93
+ state.lastHostExpanded = hostExpanded;
94
+ state.expanded = !state.expanded;
95
+ }
96
+ return state.expanded ?? false;
97
+ }
98
+
99
+ setResultAvailable(state: RowState, name: CompactToolName, available: boolean): void {
100
+ this.initializeExpansion(state, name);
101
+ state.hasResult = available;
102
+ }
103
+
104
+ syncRow(
105
+ ctx: RenderContext,
106
+ running = ctx.executionStarted && ctx.state.endedAt === undefined,
107
+ finished = false,
108
+ ): RowState {
109
+ const state = ctx.state;
110
+ if (this.restoreTiming(state, ctx.toolCallId)) running = false;
111
+ const started = !ctx.argsComplete || ctx.executionStarted || running;
112
+ if (started && state.startedAt === undefined) state.startedAt = Date.now();
113
+ if (finished && state.startedAt !== undefined && state.endedAt === undefined) state.endedAt = Date.now();
114
+ this.persistTiming(state, ctx.toolCallId);
115
+ this.syncSpinner(state, running, ctx.invalidate);
116
+ return state;
117
+ }
118
+
119
+ createTimedExecute(definition: BuiltInDefinition): TimedExecute {
120
+ const execute = definition.execute as TimedExecute;
121
+ return async (...args: any[]) => {
122
+ const toolCallId = typeof args[0] === "string" ? args[0] : undefined;
123
+ const startedAt = (toolCallId ? this.executionTimings.get(toolCallId)?.startedAt : undefined) ?? Date.now();
124
+ if (toolCallId && !this.executionTimings.has(toolCallId)) {
125
+ this.executionTimings.set(toolCallId, { startedAt });
126
+ }
127
+ try {
128
+ return await execute.apply(definition, args);
129
+ } finally {
130
+ const timing = toolCallId ? this.executionTimings.get(toolCallId) : undefined;
131
+ if (timing) timing.endedAt = Date.now();
132
+ }
133
+ };
134
+ }
135
+
136
+ private initializeExpansion(state: RowState, name: CompactToolName): boolean {
137
+ if (state.configRevision === this.configRevision) return false;
138
+ state.configRevision = this.configRevision;
139
+ state.expanded = !this.configValue.auto_compact[name];
140
+ return true;
141
+ }
142
+
143
+ private stopSpinner(state: RowState): void {
144
+ if (state.timer) clearInterval(state.timer);
145
+ state.timer = undefined;
146
+ this.spinningRows.delete(state);
147
+ }
148
+
149
+ private syncSpinner(state: RowState, running: boolean, invalidate: () => void): void {
150
+ const spinner = this.configValue.spinner;
151
+ if (!running || !this.animateRows || spinner.frames.length < 2) {
152
+ this.stopSpinner(state);
153
+ return;
154
+ }
155
+ if (state.timer) return;
156
+ state.frame ??= 0;
157
+ state.timer = setInterval(() => {
158
+ state.frame = ((state.frame ?? 0) + 1) % spinner.frames.length;
159
+ try {
160
+ invalidate();
161
+ } catch {
162
+ this.stopSpinner(state);
163
+ }
164
+ }, spinner.intervalMs);
165
+ state.timer.unref?.();
166
+ this.spinningRows.add(state);
167
+ }
168
+
169
+ private restoreTiming(state: RowState, toolCallId: string): boolean {
170
+ const timing = this.executionTimings.get(toolCallId);
171
+ if (!timing) return false;
172
+ state.startedAt = timing.startedAt;
173
+ state.endedAt = timing.endedAt;
174
+ return timing.endedAt !== undefined;
175
+ }
176
+
177
+ private persistTiming(state: RowState, toolCallId: string): void {
178
+ if (state.startedAt === undefined) return;
179
+ const timing = this.executionTimings.get(toolCallId) ?? { startedAt: state.startedAt };
180
+ timing.startedAt = state.startedAt;
181
+ if (state.endedAt !== undefined) timing.endedAt = state.endedAt;
182
+ this.executionTimings.set(toolCallId, timing);
183
+ if (this.executionTimings.size <= MAX_TRACKED_ROWS) return;
184
+ const oldest = this.executionTimings.keys().next().value;
185
+ if (oldest !== undefined) this.executionTimings.delete(oldest);
186
+ }
187
+ }
@@ -0,0 +1,39 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import type { Component } from "@earendil-works/pi-tui";
3
+ import type { DurationIndicatorConfig } from "./compact-tools-core.ts";
4
+
5
+ export const SUPPORTED_TOOLS = ["read", "write", "edit", "bash", "powershell", "grep", "find", "ls"] as const;
6
+ export const SUPPORTED_TOOL_SET = new Set<string>(SUPPORTED_TOOLS);
7
+
8
+ export type CompactToolName = (typeof SUPPORTED_TOOLS)[number];
9
+ export type ShellToolName = "bash" | "powershell";
10
+ export type ToolArgs = Record<string, unknown>;
11
+ export type BuiltInDefinition = ToolDefinition<any, any, any>;
12
+
13
+ export interface CompactToolsConfig {
14
+ tools: CompactToolName[];
15
+ auto_compact: Record<CompactToolName, boolean>;
16
+ spinner: {
17
+ frames: string[];
18
+ intervalMs: number;
19
+ };
20
+ durationIndicators: DurationIndicatorConfig[];
21
+ }
22
+
23
+ export interface RowState {
24
+ expanded?: boolean;
25
+ hasResult?: boolean;
26
+ lastHostExpanded?: boolean;
27
+ configRevision?: object;
28
+ frame?: number;
29
+ startedAt?: number;
30
+ endedAt?: number;
31
+ timer?: ReturnType<typeof setInterval>;
32
+ originalResultComponent?: Component;
33
+ }
34
+
35
+ type BaseRenderContext = Parameters<NonNullable<BuiltInDefinition["renderCall"]>>[2];
36
+ export type RenderContext<TArgs = ToolArgs> = Omit<BaseRenderContext, "args" | "state"> & {
37
+ args: TArgs;
38
+ state: RowState;
39
+ };