pi-compact-tools 0.6.0 → 0.6.3
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/README.md +183 -183
- package/extensions/compact-tools-invocation.ts +117 -69
- package/extensions/compact-tools-thinking.ts +337 -292
- package/extensions/compact-tools-types.ts +2 -0
- package/extensions/compact-tools.ts +322 -314
- package/package.json +1 -1
|
@@ -1,292 +1,337 @@
|
|
|
1
|
-
import { getAgentDir, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
isKeyRelease,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.
|
|
290
|
-
this.theme =
|
|
291
|
-
|
|
292
|
-
|
|
1
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
isKeyRelease,
|
|
6
|
+
isKeyRepeat,
|
|
7
|
+
Key,
|
|
8
|
+
matchesKey,
|
|
9
|
+
stripTerminalSequences,
|
|
10
|
+
truncateToWidth,
|
|
11
|
+
wrapTextWithAnsi,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
13
|
+
|
|
14
|
+
export type ThinkingView = "summary" | "detail" | "hidden";
|
|
15
|
+
export type ThinkingPhase = "summary" | "detail" | "summary-after-detail" | "hidden";
|
|
16
|
+
|
|
17
|
+
type ThinkingRenderOptions = {
|
|
18
|
+
styleSummary?: (summary: string, sectionIndex: number) => string;
|
|
19
|
+
styleDetailPrefix?: (prefix: string) => string;
|
|
20
|
+
controls?: string | ((hasDetail: boolean) => string | undefined);
|
|
21
|
+
styleControlPrefix?: (prefix: string) => string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const NEXT_PHASE: Record<ThinkingPhase, ThinkingPhase> = {
|
|
25
|
+
summary: "detail",
|
|
26
|
+
detail: "summary-after-detail",
|
|
27
|
+
"summary-after-detail": "hidden",
|
|
28
|
+
hidden: "summary",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function thinkingViewForPhase(phase: ThinkingPhase): ThinkingView {
|
|
32
|
+
return phase === "summary-after-detail" ? "summary" : phase;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function nextThinkingPhase(phase: ThinkingPhase): ThinkingPhase {
|
|
36
|
+
return NEXT_PHASE[phase];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ThinkingToggleInput = "toggle" | "repeat" | "release";
|
|
40
|
+
|
|
41
|
+
export function classifyThinkingToggleInput(data: string): ThinkingToggleInput | undefined {
|
|
42
|
+
if (!matchesKey(data, Key.ctrl("t"))) return undefined;
|
|
43
|
+
if (isKeyRelease(data)) return "release";
|
|
44
|
+
return isKeyRepeat(data) ? "repeat" : "toggle";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function plainSummary(line: string): string {
|
|
48
|
+
return line
|
|
49
|
+
.replace(/^\s{0,3}#{1,6}\s+/u, "")
|
|
50
|
+
.replace(/^\s*[-*+]\s+/u, "")
|
|
51
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1")
|
|
52
|
+
.replace(/[*_~`]/gu, "")
|
|
53
|
+
.replace(/\s+/gu, " ")
|
|
54
|
+
.trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ThinkingSection = { summary: string; detail: string };
|
|
58
|
+
|
|
59
|
+
function splitThinkingSections(markdown: string): ThinkingSection[] {
|
|
60
|
+
const normalized = markdown.includes("\r") ? markdown.replace(/\r\n?/gu, "\n") : markdown;
|
|
61
|
+
const lines = normalized.split("\n");
|
|
62
|
+
let first = -1;
|
|
63
|
+
let summary = "";
|
|
64
|
+
for (let index = 0; index < lines.length; index++) {
|
|
65
|
+
summary = plainSummary(lines[index]!);
|
|
66
|
+
if (summary) {
|
|
67
|
+
first = index;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (first < 0) return [{ summary: "Thinking...", detail: "" }];
|
|
72
|
+
const sections: ThinkingSection[] = [];
|
|
73
|
+
let detail: string[] = [];
|
|
74
|
+
let inFence = false;
|
|
75
|
+
const flush = () => {
|
|
76
|
+
sections.push({ summary, detail: detail.join("\n").trim() });
|
|
77
|
+
detail = [];
|
|
78
|
+
};
|
|
79
|
+
for (let index = first + 1; index < lines.length; index++) {
|
|
80
|
+
const line = lines[index]!;
|
|
81
|
+
const fence = /^\s*(```|~~~)/u.test(line);
|
|
82
|
+
if (!inFence) {
|
|
83
|
+
const boldHeading = line.match(/^\s*\*\*(.+?)\*\*\s*$/u)?.[1];
|
|
84
|
+
const markdownHeading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*$/u)?.[1];
|
|
85
|
+
const heading = boldHeading ?? markdownHeading;
|
|
86
|
+
if (heading) {
|
|
87
|
+
flush();
|
|
88
|
+
summary = plainSummary(heading);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
detail.push(line);
|
|
93
|
+
if (fence) inFence = !inFence;
|
|
94
|
+
}
|
|
95
|
+
flush();
|
|
96
|
+
return sections;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compactDetail(
|
|
100
|
+
detail: string,
|
|
101
|
+
availableWidth: number,
|
|
102
|
+
stylePrefix: (prefix: string) => string,
|
|
103
|
+
): string {
|
|
104
|
+
const output: string[] = [];
|
|
105
|
+
let inFence = false;
|
|
106
|
+
for (const logicalLine of detail.replace(/\t/gu, " ").split("\n")) {
|
|
107
|
+
const fence = /^\s*(```|~~~)/u.test(logicalLine);
|
|
108
|
+
if (!inFence && fence) {
|
|
109
|
+
// Fences need a real blockquote and blank delimiter to remain valid Markdown.
|
|
110
|
+
output.push("", `> ${logicalLine}`);
|
|
111
|
+
inFence = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (inFence) {
|
|
115
|
+
output.push(logicalLine ? `> ${logicalLine}` : ">");
|
|
116
|
+
if (fence) {
|
|
117
|
+
inFence = false;
|
|
118
|
+
output.push("");
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!logicalLine) {
|
|
123
|
+
output.push(`${stylePrefix("│")} `);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const wrapped = wrapTextWithAnsi(logicalLine, Math.max(1, availableWidth - 2));
|
|
127
|
+
for (const line of wrapped) {
|
|
128
|
+
output.push(`${stylePrefix("│ ")}${stripTerminalSequences(line)} `);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return output.join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hardBreak(lines: string[]): string {
|
|
135
|
+
return lines.map((line, index) => (index < lines.length - 1 ? `${line} ` : line)).join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function hasThinkingDetail(markdown: string): boolean {
|
|
139
|
+
return splitThinkingSections(markdown).some(({ detail }) => detail.length > 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Display-only transformation; the original thinking remains unchanged in session/model context. */
|
|
143
|
+
export function renderThinkingView(
|
|
144
|
+
markdown: string,
|
|
145
|
+
view: ThinkingView,
|
|
146
|
+
availableWidth: number,
|
|
147
|
+
options: ThinkingRenderOptions = {},
|
|
148
|
+
): string {
|
|
149
|
+
if (view === "hidden") return "Thinking...";
|
|
150
|
+
const controls = options.controls ?? ((hasDetail: boolean) =>
|
|
151
|
+
hasDetail ? "ctrl+t toggle • click to hide" : undefined);
|
|
152
|
+
const styleControlPrefix = options.styleControlPrefix ?? ((prefix: string) => prefix);
|
|
153
|
+
return splitThinkingSections(markdown)
|
|
154
|
+
.map(({ summary, detail }, sectionIndex) => {
|
|
155
|
+
const fittedSummary = stripTerminalSequences(
|
|
156
|
+
truncateToWidth(summary, Math.max(1, availableWidth), "…"),
|
|
157
|
+
);
|
|
158
|
+
const title = options.styleSummary?.(fittedSummary, sectionIndex) ?? fittedSummary;
|
|
159
|
+
const sectionControls = typeof controls === "function" ? controls(detail.length > 0) : controls;
|
|
160
|
+
const controlLines = sectionControls
|
|
161
|
+
? wrapTextWithAnsi(sectionControls, Math.max(1, availableWidth - 3)).map(
|
|
162
|
+
(line, index) => `${index === 0 ? styleControlPrefix("└─ ") : " "}${line}`,
|
|
163
|
+
)
|
|
164
|
+
: [];
|
|
165
|
+
if (view === "detail" && detail) {
|
|
166
|
+
const styleDetailPrefix = options.styleDetailPrefix ?? ((prefix: string) => prefix);
|
|
167
|
+
const connector = `${styleDetailPrefix("│")} `;
|
|
168
|
+
const detailOutput = `${title} \n${connector}\n${compactDetail(detail, availableWidth, styleDetailPrefix)}`;
|
|
169
|
+
return controlLines.length > 0 ? `${detailOutput}\n${hardBreak(controlLines)}` : detailOutput;
|
|
170
|
+
}
|
|
171
|
+
return hardBreak([title, ...controlLines]);
|
|
172
|
+
})
|
|
173
|
+
.join("\n\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getInitialThinkingPhase(): ThinkingPhase {
|
|
177
|
+
try {
|
|
178
|
+
const settings = JSON.parse(readFileSync(join(getAgentDir(), "settings.json"), "utf8")) as {
|
|
179
|
+
hideThinkingBlock?: unknown;
|
|
180
|
+
};
|
|
181
|
+
return settings.hideThinkingBlock === true ? "hidden" : "summary";
|
|
182
|
+
} catch {
|
|
183
|
+
return "summary";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function isThinkingStreamEvent(eventType: string): boolean {
|
|
188
|
+
return eventType === "thinking_start" || eventType === "thinking_delta";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const SHIMMER_FRAME_INTERVAL_MS = 80;
|
|
192
|
+
|
|
193
|
+
function shimmer(summary: string, theme: Theme, startedAt: number): string {
|
|
194
|
+
const characters = [...summary];
|
|
195
|
+
if (characters.length === 0) return summary;
|
|
196
|
+
const sweepTail = 5;
|
|
197
|
+
// Each logical section receives its own start time. The highlight therefore
|
|
198
|
+
// enters at its first character instead of inheriting another section's
|
|
199
|
+
// wall-clock phase.
|
|
200
|
+
const elapsed = Math.max(0, Date.now() - startedAt);
|
|
201
|
+
const center = Math.floor(elapsed / SHIMMER_FRAME_INTERVAL_MS) % (characters.length + sweepTail);
|
|
202
|
+
let currentColor: Parameters<Theme["fg"]>[0] | undefined;
|
|
203
|
+
let chunk = "";
|
|
204
|
+
let output = "";
|
|
205
|
+
for (let index = 0; index < characters.length; index++) {
|
|
206
|
+
const distance = Math.abs(index - center);
|
|
207
|
+
// Stay entirely in the sky-blue family: the active baseline is blue,
|
|
208
|
+
// and the sweep fades through light blue into a near-white core.
|
|
209
|
+
const color = distance === 0
|
|
210
|
+
? "userMessageText"
|
|
211
|
+
: distance === 1
|
|
212
|
+
? "text"
|
|
213
|
+
: distance === 2
|
|
214
|
+
? "thinkingMax"
|
|
215
|
+
: "thinkingXhigh";
|
|
216
|
+
if (currentColor !== undefined && color !== currentColor) {
|
|
217
|
+
output += theme.fg(currentColor, theme.bold(chunk));
|
|
218
|
+
chunk = "";
|
|
219
|
+
}
|
|
220
|
+
currentColor = color;
|
|
221
|
+
chunk += characters[index];
|
|
222
|
+
}
|
|
223
|
+
return currentColor === undefined ? output : output + theme.fg(currentColor, theme.bold(chunk));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export class ThinkingCycleController {
|
|
227
|
+
private phase: ThinkingPhase = "summary";
|
|
228
|
+
private unsubscribe: (() => void) | undefined;
|
|
229
|
+
private theme: Theme | undefined;
|
|
230
|
+
private thinkingStreamActive = false;
|
|
231
|
+
private hasToggleableThinking = false;
|
|
232
|
+
private refreshThinking: (() => void) | undefined;
|
|
233
|
+
private readonly summarySweepStartedAt = new Map<number, number>();
|
|
234
|
+
|
|
235
|
+
constructor(pi: ExtensionAPI) {
|
|
236
|
+
pi.on("message_start", () => this.resetSweep());
|
|
237
|
+
pi.on("message_update", (event) => {
|
|
238
|
+
// `context.isStreaming` describes the whole assistant message. Tool-call
|
|
239
|
+
// argument streaming therefore remains true after thinking has ended.
|
|
240
|
+
// Track the actual provider event so the sweep ends on thinking_end.
|
|
241
|
+
const eventType = event.assistantMessageEvent.type;
|
|
242
|
+
if (eventType === "thinking_start") this.summarySweepStartedAt.clear();
|
|
243
|
+
this.thinkingStreamActive = isThinkingStreamEvent(eventType);
|
|
244
|
+
});
|
|
245
|
+
pi.on("message_end", () => this.resetSweep());
|
|
246
|
+
pi.on("tool_call", () => this.resetSweep());
|
|
247
|
+
|
|
248
|
+
pi.registerMarkdownTransformer((markdown, context) => {
|
|
249
|
+
if (context.messageType !== "assistant-thinking") return markdown;
|
|
250
|
+
this.hasToggleableThinking ||= hasThinkingDetail(markdown);
|
|
251
|
+
const theme = this.theme;
|
|
252
|
+
const detailTextPrefix = theme?.getFgAnsi("thinkingText");
|
|
253
|
+
return renderThinkingView(markdown, thinkingViewForPhase(this.phase), context.availableWidth, {
|
|
254
|
+
styleSummary: theme
|
|
255
|
+
? context.isStreaming && this.thinkingStreamActive
|
|
256
|
+
? (summary, sectionIndex) => {
|
|
257
|
+
let startedAt = this.summarySweepStartedAt.get(sectionIndex);
|
|
258
|
+
if (startedAt === undefined) {
|
|
259
|
+
startedAt = Date.now();
|
|
260
|
+
this.summarySweepStartedAt.set(sectionIndex, startedAt);
|
|
261
|
+
}
|
|
262
|
+
return shimmer(summary, theme, startedAt);
|
|
263
|
+
}
|
|
264
|
+
: (summary) => theme.fg("thinkingMax", theme.bold(summary))
|
|
265
|
+
: undefined,
|
|
266
|
+
// border styling resets the foreground; resume detail gray immediately.
|
|
267
|
+
styleDetailPrefix: theme && detailTextPrefix
|
|
268
|
+
? (prefix) => theme.fg("border", prefix) + detailTextPrefix
|
|
269
|
+
: undefined,
|
|
270
|
+
controls: theme
|
|
271
|
+
? (hasDetail) => hasDetail
|
|
272
|
+
? theme.fg(
|
|
273
|
+
"borderAccent",
|
|
274
|
+
`${theme.italic("ctrl+t")} toggle • ${theme.italic("click")} to hide`,
|
|
275
|
+
)
|
|
276
|
+
: undefined
|
|
277
|
+
: undefined,
|
|
278
|
+
styleControlPrefix: theme ? (prefix) => theme.fg("border", prefix) : undefined,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private resetSweep(): void {
|
|
284
|
+
this.thinkingStreamActive = false;
|
|
285
|
+
this.summarySweepStartedAt.clear();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
bind(ctx: ExtensionContext): void {
|
|
289
|
+
this.unsubscribe?.();
|
|
290
|
+
this.theme = ctx.ui.theme;
|
|
291
|
+
this.thinkingStreamActive = false;
|
|
292
|
+
this.hasToggleableThinking = false;
|
|
293
|
+
this.summarySweepStartedAt.clear();
|
|
294
|
+
// Match Pi's persisted host visibility after startup or /reload. When the
|
|
295
|
+
// host is hidden, Markdown transformers are not invoked at all. Pi wraps
|
|
296
|
+
// this label in thinkingText, so an inner thinkingMax span is required to
|
|
297
|
+
// keep it consistent with completed summary titles.
|
|
298
|
+
this.phase = getInitialThinkingPhase();
|
|
299
|
+
const hiddenLabel = this.theme.fg("thinkingMax", this.theme.bold("Thinking..."));
|
|
300
|
+
// Pi does not currently expose transcript invalidation directly. Updating
|
|
301
|
+
// this label rebuilds assistant Markdown and requests a render without
|
|
302
|
+
// changing the message or its hidden/visible state.
|
|
303
|
+
this.refreshThinking = () => ctx.ui.setHiddenThinkingLabel(hiddenLabel);
|
|
304
|
+
this.refreshThinking();
|
|
305
|
+
this.unsubscribe = ctx.ui.onTerminalInput((data) => {
|
|
306
|
+
const input = classifyThinkingToggleInput(data);
|
|
307
|
+
if (input === undefined) return undefined;
|
|
308
|
+
// Kitty-capable macOS terminals report held keys as repeat events. Treating
|
|
309
|
+
// those as presses can skip detail before the user sees it.
|
|
310
|
+
if (input === "release" || input === "repeat") return { consume: true };
|
|
311
|
+
// With no provider-supplied detail, leave Ctrl+T to Pi's normal
|
|
312
|
+
// visible/hidden toggle instead of inserting an identical detail phase.
|
|
313
|
+
if (!this.hasToggleableThinking) return undefined;
|
|
314
|
+
this.phase = nextThinkingPhase(this.phase);
|
|
315
|
+
if (this.phase === "detail" || this.phase === "summary-after-detail") {
|
|
316
|
+
// Visible-to-visible transitions are owned by this extension. Rebuild the
|
|
317
|
+
// Markdown and prevent Pi's built-in two-state toggle from hiding it.
|
|
318
|
+
this.refreshThinking?.();
|
|
319
|
+
return { consume: true };
|
|
320
|
+
}
|
|
321
|
+
// summary-after-detail -> hidden and hidden -> summary intentionally reach
|
|
322
|
+
// Pi's existing two-state handler. This keeps visibility/settings in sync
|
|
323
|
+
// and clears built-in per-block click overrides.
|
|
324
|
+
return undefined;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
dispose(): void {
|
|
329
|
+
this.unsubscribe?.();
|
|
330
|
+
this.unsubscribe = undefined;
|
|
331
|
+
this.refreshThinking = undefined;
|
|
332
|
+
this.thinkingStreamActive = false;
|
|
333
|
+
this.hasToggleableThinking = false;
|
|
334
|
+
this.summarySweepStartedAt.clear();
|
|
335
|
+
this.theme = undefined;
|
|
336
|
+
}
|
|
337
|
+
}
|