@pi-kaush/pi-tool-call-markers 0.2.4 → 0.2.6

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.
@@ -2,18 +2,34 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  const THINKING_GROUPING_PATCHED = Symbol.for("kg.pi.thinkingGrouping.v1");
5
+ const PI_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
+ const SPINNER_INTERVAL_MS = 80;
5
7
 
6
8
  type AssistantMessageLike = {
7
9
  content?: unknown[];
8
10
  };
9
11
 
10
12
  type AssistantMessageRow = {
11
- updateContent(message: AssistantMessageLike): void;
13
+ hiddenThinkingLabel?: unknown;
14
+ hideThinkingBlock?: unknown;
15
+ updateContent(message: AssistantMessageLike, ...args: unknown[]): void;
16
+ };
17
+
18
+ type ThinkingTiming = {
19
+ finishedAt?: number;
20
+ startedAt: number;
12
21
  };
13
22
 
14
23
  type ThinkingGroupingPatchState = {
15
- originalUpdateContent: (message: AssistantMessageLike) => void;
16
- patchedUpdateContent?: (message: AssistantMessageLike) => void;
24
+ originalUpdateContent: (
25
+ message: AssistantMessageLike,
26
+ ...args: unknown[]
27
+ ) => void;
28
+ patchedUpdateContent?: (
29
+ message: AssistantMessageLike,
30
+ ...args: unknown[]
31
+ ) => void;
32
+ timings: WeakMap<AssistantMessageRow, ThinkingTiming>;
17
33
  };
18
34
 
19
35
  type ThinkingContentLike = {
@@ -31,6 +47,16 @@ function isThinkingContent(content: unknown): content is ThinkingContentLike {
31
47
  );
32
48
  }
33
49
 
50
+ function hasThinkingContent(message: AssistantMessageLike): boolean {
51
+ return (
52
+ Array.isArray(message.content) &&
53
+ message.content.some(
54
+ (content) =>
55
+ isThinkingContent(content) && content.thinking.trim().length > 0,
56
+ )
57
+ );
58
+ }
59
+
34
60
  function combineAdjacentThinking(
35
61
  message: AssistantMessageLike,
36
62
  ): AssistantMessageLike {
@@ -55,6 +81,69 @@ function combineAdjacentThinking(
55
81
  return changed ? { ...message, content } : message;
56
82
  }
57
83
 
84
+ function formatThoughtDuration(startedAt: number, finishedAt: number): string {
85
+ return `${(Math.max(0, finishedAt - startedAt) / 1000).toFixed(1)}s`;
86
+ }
87
+
88
+ function thinkingSpinner(startedAt: number, now: number): string {
89
+ const frame = Math.floor(Math.max(0, now - startedAt) / SPINNER_INTERVAL_MS);
90
+ return PI_SPINNER_FRAMES[frame % PI_SPINNER_FRAMES.length]!;
91
+ }
92
+
93
+ function lifecycleLabel(
94
+ row: AssistantMessageRow,
95
+ streaming: boolean | undefined,
96
+ timings: WeakMap<AssistantMessageRow, ThinkingTiming>,
97
+ ): string {
98
+ const now = Date.now();
99
+ let current = timings.get(row);
100
+ if (streaming === true) {
101
+ if (!current || current.finishedAt !== undefined) {
102
+ current = { startedAt: now };
103
+ timings.set(row, current);
104
+ }
105
+ return `${thinkingSpinner(current.startedAt, now)} Thinking…`;
106
+ }
107
+
108
+ if (streaming === undefined && current?.finishedAt === undefined && current) {
109
+ // Pi can rebuild a live row on resize/theme changes without forwarding
110
+ // the optional streaming flag. Keep its active label until an explicit
111
+ // final update arrives.
112
+ return `${thinkingSpinner(current.startedAt, now)} Thinking…`;
113
+ }
114
+
115
+ if (streaming === false && current?.finishedAt === undefined) {
116
+ if (current) current.finishedAt = now;
117
+ }
118
+
119
+ const settled = timings.get(row);
120
+ if (settled?.finishedAt !== undefined) {
121
+ return `+ Thought · ${formatThoughtDuration(settled.startedAt, settled.finishedAt)}`;
122
+ }
123
+ return "+ Thought";
124
+ }
125
+
126
+ function applyHiddenThinkingLabel(
127
+ row: AssistantMessageRow,
128
+ message: AssistantMessageLike,
129
+ streaming: boolean | undefined,
130
+ timings: WeakMap<AssistantMessageRow, ThinkingTiming>,
131
+ ): void {
132
+ // Record the component's first streaming update even when the provider has
133
+ // not emitted a non-empty thinking block yet.
134
+ const label = lifecycleLabel(row, streaming, timings);
135
+ if (!hasThinkingContent(message)) return;
136
+ if (
137
+ row.hideThinkingBlock !== true ||
138
+ typeof row.hiddenThinkingLabel !== "string"
139
+ ) {
140
+ return;
141
+ }
142
+ // Assign the row-local field directly. The public UI setter relabels every
143
+ // historical assistant row and would make earlier durations change.
144
+ row.hiddenThinkingLabel = label;
145
+ }
146
+
58
147
  // TODO: Replace prototype patching with a public assistant-message rendering API.
59
148
  function installThinkingGroupingPatch():
60
149
  | ThinkingGroupingPatchState
@@ -63,7 +152,10 @@ function installThinkingGroupingPatch():
63
152
  const proto =
64
153
  AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
65
154
  [THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
66
- updateContent?: (message: AssistantMessageLike) => void;
155
+ updateContent?: (
156
+ message: AssistantMessageLike,
157
+ ...args: unknown[]
158
+ ) => void;
67
159
  };
68
160
  if (!proto || typeof proto.updateContent !== "function") return undefined;
69
161
 
@@ -72,19 +164,28 @@ function installThinkingGroupingPatch():
72
164
 
73
165
  const state: ThinkingGroupingPatchState = {
74
166
  originalUpdateContent: proto.updateContent,
167
+ timings: new WeakMap(),
75
168
  };
76
169
  const patchedUpdateContent = function updateContentWithCombinedThinking(
77
170
  this: AssistantMessageRow,
78
171
  message: AssistantMessageLike,
172
+ ...args: unknown[]
79
173
  ): void {
80
- // Thinking grouping is cosmetic. If it fails, render the original once.
174
+ // Grouping and labels are cosmetic. Each fails open independently, and
175
+ // the original renderer is still called exactly once with every arg.
81
176
  let combined = message;
82
177
  try {
83
178
  combined = combineAdjacentThinking(message);
84
179
  } catch {
85
180
  // Preserve the original message intact.
86
181
  }
87
- state.originalUpdateContent.call(this, combined);
182
+ try {
183
+ const streaming = typeof args[0] === "boolean" ? args[0] : undefined;
184
+ applyHiddenThinkingLabel(this, combined, streaming, state.timings);
185
+ } catch {
186
+ // Preserve Pi's native label if its private row shape changes.
187
+ }
188
+ Reflect.apply(state.originalUpdateContent, this, [combined, ...args]);
88
189
  };
89
190
 
90
191
  state.patchedUpdateContent = patchedUpdateContent;
@@ -106,13 +207,17 @@ function uninstallThinkingGroupingPatch(
106
207
  const proto =
107
208
  AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
108
209
  [THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
109
- updateContent?: (message: AssistantMessageLike) => void;
210
+ updateContent?: (
211
+ message: AssistantMessageLike,
212
+ ...args: unknown[]
213
+ ) => void;
110
214
  };
111
215
  if (
112
216
  proto[THINKING_GROUPING_PATCHED] !== state ||
113
217
  proto.updateContent !== state.patchedUpdateContent
114
- )
218
+ ) {
115
219
  return;
220
+ }
116
221
  proto.updateContent = state.originalUpdateContent;
117
222
  delete proto[THINKING_GROUPING_PATCHED];
118
223
  }