pi-compact-tools 0.6.3 → 0.7.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.
@@ -9,32 +9,26 @@ import type {
9
9
  } from "./compact-tools-types.ts";
10
10
 
11
11
  const MAX_TRACKED_ROWS = 2_000;
12
- const SPINNING_ROWS_KEY = Symbol.for("pi.compact-tools.spinning-rows");
12
+ const INDICATOR_INTERVAL_MS = 45;
13
13
  const EXECUTION_TIMINGS_KEY = Symbol.for("pi.compact-tools.execution-timings");
14
14
 
15
15
  type ExecutionTiming = { startedAt: number; endedAt?: number };
16
- type TrackedRow = { state: RowState; name: CompactToolName; invalidate: () => void };
17
16
  type TimedExecute = (...args: any[]) => Promise<AgentToolResult<unknown>>;
18
17
  type SharedState = typeof globalThis & {
19
- [SPINNING_ROWS_KEY]?: Set<RowState>;
20
18
  [EXECUTION_TIMINGS_KEY]?: Map<string, ExecutionTiming>;
21
19
  };
22
20
 
23
21
  export class ToolRuntime {
24
22
  private configValue = DEFAULT_CONFIG;
25
23
  private configRevision: object = {};
26
- private animateRows = false;
27
- private readonly spinningRows = new Set<RowState>();
28
24
  private readonly executionTimings: Map<string, ExecutionTiming>;
29
- private readonly trackedRows = new Map<string, TrackedRow>();
25
+ private readonly indicatorInvalidators = new Map<string, () => void>();
26
+ private indicatorFrame = 0;
27
+ private indicatorTimer: ReturnType<typeof setInterval> | undefined;
30
28
 
31
29
  constructor() {
32
30
  const shared = globalThis as SharedState;
33
- for (const state of shared[SPINNING_ROWS_KEY] ?? []) {
34
- if (state.timer) clearInterval(state.timer);
35
- }
36
31
  this.executionTimings = shared[EXECUTION_TIMINGS_KEY] ?? new Map<string, ExecutionTiming>();
37
- shared[SPINNING_ROWS_KEY] = this.spinningRows;
38
32
  shared[EXECUTION_TIMINGS_KEY] = this.executionTimings;
39
33
  }
40
34
 
@@ -42,14 +36,8 @@ export class ToolRuntime {
42
36
  return this.configValue;
43
37
  }
44
38
 
45
- get animatesRows(): boolean {
46
- return this.animateRows;
47
- }
48
-
49
- configure(config: CompactToolsConfig, animateRows: boolean): void {
50
- this.reset(false);
39
+ configure(config: CompactToolsConfig): void {
51
40
  this.configValue = config;
52
- this.animateRows = animateRows;
53
41
  this.configRevision = {};
54
42
  }
55
43
 
@@ -58,40 +46,35 @@ export class ToolRuntime {
58
46
  }
59
47
 
60
48
  reset(clearTimings: boolean): void {
61
- for (const state of this.spinningRows) this.stopSpinner(state);
62
- this.trackedRows.clear();
49
+ this.stopIndicators();
63
50
  if (clearTimings) this.clearTimings();
64
51
  }
65
52
 
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);
53
+ syncIndicator(toolCallId: string, running: boolean, invalidate: () => void): number {
54
+ if (!running) {
55
+ this.removeIndicator(toolCallId);
56
+ return 0;
78
57
  }
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();
58
+ this.indicatorInvalidators.set(toolCallId, invalidate);
59
+ if (!this.indicatorTimer) {
60
+ this.indicatorTimer = setInterval(() => {
61
+ this.indicatorFrame++;
62
+ for (const requestRender of this.indicatorInvalidators.values()) requestRender();
63
+ }, INDICATOR_INTERVAL_MS);
64
+ this.indicatorTimer.unref?.();
84
65
  }
85
- return expand ? "expanded" : "collapsed";
66
+ return this.indicatorFrame;
86
67
  }
87
68
 
88
69
  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) {
70
+ const initialized = this.initializeExpansion(state, name, hostExpanded);
71
+ if (!initialized && state.lastHostExpanded !== hostExpanded) {
93
72
  state.lastHostExpanded = hostExpanded;
94
- state.expanded = !state.expanded;
73
+ state.expanded = hostExpanded;
74
+ // A non-auto-compacted row collapses back to its bounded preview. Hiding it
75
+ // here removes the clicked row from under the pointer, so the next click can
76
+ // accidentally hit the Thinking block that moved into the same coordinates.
77
+ state.preview = !hostExpanded && !this.configValue.auto_compact[name];
95
78
  }
96
79
  return state.expanded ?? false;
97
80
  }
@@ -111,8 +94,8 @@ export class ToolRuntime {
111
94
  const started = !ctx.argsComplete || ctx.executionStarted || running;
112
95
  if (started && state.startedAt === undefined) state.startedAt = Date.now();
113
96
  if (finished && state.startedAt !== undefined && state.endedAt === undefined) state.endedAt = Date.now();
97
+ if (finished) this.removeIndicator(ctx.toolCallId);
114
98
  this.persistTiming(state, ctx.toolCallId);
115
- this.syncSpinner(state, running, ctx.invalidate);
116
99
  return state;
117
100
  }
118
101
 
@@ -133,37 +116,28 @@ export class ToolRuntime {
133
116
  };
134
117
  }
135
118
 
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;
119
+ private removeIndicator(toolCallId: string): void {
120
+ this.indicatorInvalidators.delete(toolCallId);
121
+ if (this.indicatorInvalidators.size > 0 || !this.indicatorTimer) return;
122
+ clearInterval(this.indicatorTimer);
123
+ this.indicatorTimer = undefined;
124
+ this.indicatorFrame = 0;
141
125
  }
142
126
 
143
- private stopSpinner(state: RowState): void {
144
- if (state.timer) clearInterval(state.timer);
145
- state.timer = undefined;
146
- this.spinningRows.delete(state);
127
+ private stopIndicators(): void {
128
+ if (this.indicatorTimer) clearInterval(this.indicatorTimer);
129
+ this.indicatorTimer = undefined;
130
+ this.indicatorInvalidators.clear();
131
+ this.indicatorFrame = 0;
147
132
  }
148
133
 
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);
134
+ private initializeExpansion(state: RowState, name: CompactToolName, hostExpanded = false): boolean {
135
+ if (state.configRevision === this.configRevision) return false;
136
+ state.configRevision = this.configRevision;
137
+ state.lastHostExpanded = hostExpanded;
138
+ state.expanded = hostExpanded;
139
+ state.preview = !hostExpanded && !this.configValue.auto_compact[name];
140
+ return true;
167
141
  }
168
142
 
169
143
  private restoreTiming(state: RowState, toolCallId: string): boolean {