@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.
- package/CHANGELOG.md +21 -14
- package/README.md +58 -31
- package/package.json +1 -1
- package/src/container-hooks.ts +57 -0
- package/src/index.ts +475 -422
- package/src/thinking-block-merger.ts +113 -8
|
@@ -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
|
-
|
|
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: (
|
|
16
|
-
|
|
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?: (
|
|
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
|
-
//
|
|
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
|
-
|
|
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?: (
|
|
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
|
}
|