@fyeeme/pi-thinking-ui 1.0.1 → 1.1.0
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 +13 -0
- package/index.ts +35 -123
- package/markdown-render.ts +104 -0
- package/package.json +8 -9
- package/state.ts +1 -291
- package/types.ts +0 -6
- package/internal-patch.ts +0 -445
- package/render.ts +0 -488
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.1.0] - 2026-08-08
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
|
|
7
|
+
- Internal monkeypatch of pi compiled modules (`internal-patch.ts`, `render.ts`) — thinking rendering no longer imports `dist/modes/interactive/...` internals; it now uses the provided `registerMarkdownTransformer` hook only.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Thinking rendering rebuilt on `registerMarkdownTransformer` (new `markdown-render.ts`): `collapsed` → one blockquote line (role icon + summary), `summary` → bullet list of derived step summaries (capped, `… (+N more)` tail), `expanded` → passthrough. Reuses the pure-heuristic `parse.ts` engine.
|
|
12
|
+
- Streaming: the full heuristic derivation now runs only on finalize/restore/resize; during streaming a cheap first-line view is shown for `collapsed` (pi's transformer contract requires synchronous, inexpensive transformers).
|
|
13
|
+
- `state.ts` slimmed: removed patch bookkeeping and the now-unused active-thinking-state / message-scope-ownership subsystems.
|
|
14
|
+
- Mode switching re-renders via the existing `refreshThinkingUI` (`setHiddenThinkingLabel` toggle); mode resolution for rendering is process-global (transformer context carries no message/scope identity).
|
|
15
|
+
|
|
3
16
|
## [1.0.1] - 2025-07-25
|
|
4
17
|
|
|
5
18
|
### Changed
|
package/index.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Key } from "@earendil-works/pi-tui";
|
|
4
|
-
import { retainThinkingUIPatch } from "./internal-patch.ts";
|
|
5
4
|
import { clearThinkingUIModePreference, readThinkingUIModePreference, writeThinkingUIModePreference } from "./persistence.ts";
|
|
6
5
|
import { parseThinkingMode } from "./parse.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { streamingThinkingMarkdown, thinkingToMarkdown } from "./markdown-render.ts";
|
|
7
|
+
import {
|
|
8
|
+
getThinkingUIMode,
|
|
9
|
+
nextThinkingRefreshLabel,
|
|
10
|
+
setCurrentThinkingScopeKey,
|
|
11
|
+
setThinkingUIMode,
|
|
12
|
+
} from "./state.ts";
|
|
8
13
|
import type { PersistedThinkingUIPreferenceScope, ThinkingUIMode } from "./types.ts";
|
|
9
14
|
|
|
10
15
|
type ThinkingUICommandScope = "session" | PersistedThinkingUIPreferenceScope;
|
|
@@ -66,6 +71,14 @@ async function restoreMode(ctx: ExtensionContext): Promise<ThinkingUIMode> {
|
|
|
66
71
|
return globalMode ?? "collapsed";
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Force every AssistantMessageComponent to re-run updateContent (and thus the
|
|
76
|
+
* markdown transformer) by toggling the hidden-thinking label through pi's
|
|
77
|
+
* ctx.ui proxy. `nextThinkingRefreshLabel` appends/toggles an invisible suffix
|
|
78
|
+
* so the label string changes on every call — without it, setHiddenThinkingLabel
|
|
79
|
+
* would no-op when the visible label is unchanged and the new mode would not
|
|
80
|
+
* take effect until the next natural re-render.
|
|
81
|
+
*/
|
|
69
82
|
function refreshThinkingUI(ctx: ExtensionContext): void {
|
|
70
83
|
if (!ctx.hasUI) return;
|
|
71
84
|
setCurrentThinkingScopeKey(ctx.cwd);
|
|
@@ -175,28 +188,25 @@ function reportPersistenceError(ctx: ExtensionContext, error: unknown): void {
|
|
|
175
188
|
notifyUser(ctx, `Thinking UI persistence error: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
176
189
|
}
|
|
177
190
|
|
|
178
|
-
function reportPatchError(ctx: ExtensionContext, error: unknown): void {
|
|
179
|
-
notifyUser(ctx, `Thinking UI patch error: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
191
|
export default function thinkingUIExtension(pi: ExtensionAPI): void {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
// Render thinking via the provided markdown-transformer hook (no monkeypatch).
|
|
193
|
+
// The native Markdown renderer draws whatever we return, styled as thinking
|
|
194
|
+
// text. Re-runs on streaming, finalize, restore, resize, and whenever
|
|
195
|
+
// refreshThinkingUI toggles the hidden-thinking label (mode switch).
|
|
196
|
+
//
|
|
197
|
+
// Scope caveat: MarkdownTransformContext carries no message/scope identity, so
|
|
198
|
+
// the transformer resolves mode from the process-global currentScopeKey. The
|
|
199
|
+
// per-scope mode map still serves the command/shortcut handlers (which have
|
|
200
|
+
// ctx.cwd); only rendering is process-global. Fine while pi is single-session
|
|
201
|
+
// interactive; a host-side scope id would be needed to fix (out of scope here).
|
|
202
|
+
pi.registerMarkdownTransformer((markdown, context) => {
|
|
203
|
+
if (context.messageType !== "assistant-thinking") return markdown;
|
|
204
|
+
const mode = getThinkingUIMode();
|
|
205
|
+
// Heavy heuristic derivation only on finalize/restore/resize; during
|
|
206
|
+
// streaming (every chunk) stay cheap per pi's transformer contract.
|
|
207
|
+
if (context.isStreaming) return streamingThinkingMarkdown(markdown, mode);
|
|
208
|
+
return thinkingToMarkdown(markdown, mode);
|
|
209
|
+
});
|
|
200
210
|
|
|
201
211
|
pi.registerCommand("thinking-ui", {
|
|
202
212
|
description: "Switch thinking view or set/clear project/global defaults",
|
|
@@ -208,7 +218,6 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
|
|
|
208
218
|
return;
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
const degraded = isSessionDegraded(ctx.cwd);
|
|
212
221
|
if (action.type === "clear") {
|
|
213
222
|
try {
|
|
214
223
|
await clearThinkingUIModePreference(action.scope, ctx.cwd);
|
|
@@ -217,11 +226,6 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
|
|
|
217
226
|
return;
|
|
218
227
|
}
|
|
219
228
|
|
|
220
|
-
if (degraded) {
|
|
221
|
-
notifyUser(ctx, futureCompatibleSessionMessage(action.scope, "cleared"), "info");
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
229
|
refreshThinkingUI(ctx);
|
|
226
230
|
notifyUser(ctx, `Cleared ${action.scope} thinking view default`, "info");
|
|
227
231
|
return;
|
|
@@ -241,15 +245,6 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
|
|
|
241
245
|
}
|
|
242
246
|
}
|
|
243
247
|
|
|
244
|
-
if (degraded) {
|
|
245
|
-
if (action.scope === "session") {
|
|
246
|
-
notifyUser(ctx, degradedSessionMessage(), "warning");
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
notifyUser(ctx, futureCompatibleSessionMessage(action.scope, "saved"), "info");
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
248
|
applyMode(pi, ctx, selectedMode, { announceScope: action.scope });
|
|
254
249
|
},
|
|
255
250
|
});
|
|
@@ -257,104 +252,21 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
|
|
|
257
252
|
pi.registerShortcut(Key.alt("t"), {
|
|
258
253
|
description: "Cycle thinking view (collapsed, summary, expanded)",
|
|
259
254
|
handler: async (ctx) => {
|
|
260
|
-
if (isSessionDegraded(ctx.cwd)) {
|
|
261
|
-
notifyUser(ctx, degradedSessionMessage(), "warning");
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
255
|
const nextMode = cycleMode(getThinkingUIMode(ctx.cwd));
|
|
265
256
|
applyMode(pi, ctx, nextMode, { announceScope: "session" });
|
|
266
257
|
},
|
|
267
258
|
});
|
|
268
259
|
|
|
269
260
|
pi.on("session_start", async (_event, ctx) => {
|
|
270
|
-
|
|
271
|
-
clearActiveThinkingState(undefined, activeScopeKey);
|
|
272
|
-
try {
|
|
273
|
-
registerThinkingPatchRelease(activeScopeKey, await retainThinkingUIPatch());
|
|
274
|
-
markSessionDegraded(activeScopeKey, false);
|
|
275
|
-
} catch (error) {
|
|
276
|
-
markSessionDegraded(activeScopeKey, true);
|
|
277
|
-
reportPatchError(ctx, error);
|
|
278
|
-
notifyUser(ctx, degradedSessionMessage(), "warning");
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
261
|
+
setCurrentThinkingScopeKey(ctx.cwd);
|
|
282
262
|
const restoredMode = await restoreMode(ctx);
|
|
283
263
|
applyMode(pi, ctx, restoredMode, { persistSession: false });
|
|
284
264
|
});
|
|
285
265
|
|
|
286
|
-
pi.on("message_start", async (event) => {
|
|
287
|
-
if (event.message.role === "assistant") {
|
|
288
|
-
recordThinkingMessageScope(event.message, sessionScopeKey);
|
|
289
|
-
const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
|
|
290
|
-
const timestamp = typeof (event.message as { timestamp?: unknown }).timestamp === "number"
|
|
291
|
-
? (event.message as { timestamp: number }).timestamp
|
|
292
|
-
: undefined;
|
|
293
|
-
clearActiveThinkingState(timestamp, ownerScopeKey);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
pi.on("message_update", async (event) => {
|
|
298
|
-
if (event.message.role !== "assistant") return;
|
|
299
|
-
recordThinkingMessageScope(event.message, sessionScopeKey);
|
|
300
|
-
const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
|
|
301
|
-
const assistantEvent = event.assistantMessageEvent;
|
|
302
|
-
if (assistantEvent.type === "thinking_start" || assistantEvent.type === "thinking_delta") {
|
|
303
|
-
setActiveThinkingState({
|
|
304
|
-
active: true,
|
|
305
|
-
messageTimestamp: event.message.timestamp,
|
|
306
|
-
contentIndex: assistantEvent.contentIndex,
|
|
307
|
-
}, ownerScopeKey);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
assistantEvent.type === "thinking_end" ||
|
|
313
|
-
assistantEvent.type === "text_start" ||
|
|
314
|
-
assistantEvent.type === "text_delta" ||
|
|
315
|
-
assistantEvent.type === "text_end" ||
|
|
316
|
-
assistantEvent.type === "toolcall_start" ||
|
|
317
|
-
assistantEvent.type === "toolcall_delta" ||
|
|
318
|
-
assistantEvent.type === "toolcall_end"
|
|
319
|
-
) {
|
|
320
|
-
clearActiveThinkingState(event.message.timestamp, ownerScopeKey);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
pi.on("message_end", async (event) => {
|
|
325
|
-
if (event.message.role === "assistant") {
|
|
326
|
-
recordThinkingMessageScope(event.message, sessionScopeKey);
|
|
327
|
-
const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
|
|
328
|
-
const timestamp = typeof (event.message as { timestamp?: unknown }).timestamp === "number"
|
|
329
|
-
? (event.message as { timestamp: number }).timestamp
|
|
330
|
-
: undefined;
|
|
331
|
-
clearActiveThinkingState(timestamp, ownerScopeKey);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
pi.on("agent_end", async () => {
|
|
336
|
-
clearActiveThinkingState(undefined, sessionScopeKey);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
266
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
340
|
-
|
|
341
|
-
clearActiveThinkingState(undefined, activeScopeKey);
|
|
342
|
-
clearThinkingMessageOwnership(activeScopeKey);
|
|
343
|
-
markSessionDegraded(activeScopeKey, false);
|
|
267
|
+
setCurrentThinkingScopeKey(ctx.cwd);
|
|
344
268
|
if (ctx.hasUI) {
|
|
345
269
|
ctx.ui.setStatus("thinking-ui", undefined);
|
|
346
270
|
}
|
|
347
|
-
|
|
348
|
-
const releasePatch = takeThinkingPatchRelease(activeScopeKey);
|
|
349
|
-
if (!releasePatch) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
await releasePatch();
|
|
355
|
-
} catch (error) {
|
|
356
|
-
registerThinkingPatchRelease(activeScopeKey, releasePatch);
|
|
357
|
-
reportPatchError(ctx, error);
|
|
358
|
-
}
|
|
359
271
|
});
|
|
360
272
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thinking-ui markdown emitter.
|
|
3
|
+
*
|
|
4
|
+
* Converts raw thinking text into the markdown string the registered
|
|
5
|
+
* transformer returns, per the current ThinkingUIMode. Reuses the pure
|
|
6
|
+
* heuristic engine in parse.ts (summarize / split / role) — no LLM, no async.
|
|
7
|
+
*
|
|
8
|
+
* Trade-off vs the removed ThinkingUIComponent (ANSI tree): the native
|
|
9
|
+
* Markdown renderer draws whatever markdown we return, styled as thinking
|
|
10
|
+
* text (dim/italic). The bespoke connector tree (├─└─) and live activity
|
|
11
|
+
* pulse are gone; the summary text + per-step role icons survive.
|
|
12
|
+
*/
|
|
13
|
+
import type { ThinkingUIMode } from "./types.ts";
|
|
14
|
+
import { deriveStepCore, splitThinkingIntoStepTexts } from "./parse.ts";
|
|
15
|
+
|
|
16
|
+
/** Cap the summary-mode list so a long thinking run stays readable. */
|
|
17
|
+
const SUMMARY_LIST_LIMIT = 8;
|
|
18
|
+
/** Max chars of the first line shown for collapsed mode while streaming. */
|
|
19
|
+
const STREAMING_LINE_MAX = 80;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LRU result cache for the finalize-path derivation. The transformer re-runs
|
|
23
|
+
* on every re-render where the cached width changed (e.g. terminal resize) —
|
|
24
|
+
* the thinking text is unchanged, so the result is fully reusable. Keyed by
|
|
25
|
+
* (mode, text) since thinkingToMarkdown output does not depend on width.
|
|
26
|
+
* Bounded to keep memory in check (each key holds a copy of the thinking text).
|
|
27
|
+
*/
|
|
28
|
+
const RESULT_CACHE_LIMIT = 50;
|
|
29
|
+
const resultCache = new Map<string, string>();
|
|
30
|
+
|
|
31
|
+
/** One blockquote line: role icon + summary. Derived via the fused parse.ts core. */
|
|
32
|
+
function collapseLine(text: string): string {
|
|
33
|
+
const core = deriveStepCore(text);
|
|
34
|
+
return `> ${core.icon} ${core.summary}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cheap view while the assistant message is still streaming, before the full
|
|
39
|
+
* heuristic derivation runs on finalize. collapsed → first line (keeps the
|
|
40
|
+
* "hide the bulk" intent without the O(n²) MMR cost); summary/expanded → pass
|
|
41
|
+
* through so the user can read the live stream. The transformer must stay
|
|
42
|
+
* "synchronous and inexpensive" (pi docs) and runs on every streaming chunk.
|
|
43
|
+
*/
|
|
44
|
+
export function streamingThinkingMarkdown(markdown: string, mode: ThinkingUIMode): string {
|
|
45
|
+
if (mode !== "collapsed") return markdown;
|
|
46
|
+
const line = markdown.split("\n").map((l) => l.trim()).find(Boolean);
|
|
47
|
+
if (!line) return markdown;
|
|
48
|
+
return `> ${line.length > STREAMING_LINE_MAX ? `${line.slice(0, STREAMING_LINE_MAX - 1)}…` : line}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Transform a run of thinking markdown for the given mode (finalize/restore).
|
|
53
|
+
*
|
|
54
|
+
* - expanded: pass through verbatim (native renderer handles full text).
|
|
55
|
+
* - collapsed: one blockquote line — role icon + the single best summary.
|
|
56
|
+
* - summary: a markdown bullet list of derived step summaries (icon + summary),
|
|
57
|
+
* capped at SUMMARY_LIST_LIMIT with a `… (+N more)` tail.
|
|
58
|
+
*
|
|
59
|
+
* Result is memoized per (mode, text) — the expensive MMR derivation is not
|
|
60
|
+
* re-run for unchanged text across re-renders (resize, restore, …).
|
|
61
|
+
*/
|
|
62
|
+
export function thinkingToMarkdown(markdown: string, mode: ThinkingUIMode): string {
|
|
63
|
+
const cacheKey = `${mode}\u0000${markdown}`;
|
|
64
|
+
const cached = resultCache.get(cacheKey);
|
|
65
|
+
if (cached !== undefined) {
|
|
66
|
+
// LRU refresh: move to the newest slot.
|
|
67
|
+
resultCache.delete(cacheKey);
|
|
68
|
+
resultCache.set(cacheKey, cached);
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = computeThinkingToMarkdown(markdown, mode);
|
|
73
|
+
|
|
74
|
+
resultCache.set(cacheKey, result);
|
|
75
|
+
if (resultCache.size > RESULT_CACHE_LIMIT) {
|
|
76
|
+
const oldestKey = resultCache.keys().next().value;
|
|
77
|
+
if (oldestKey !== undefined) {
|
|
78
|
+
resultCache.delete(oldestKey);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function computeThinkingToMarkdown(markdown: string, mode: ThinkingUIMode): string {
|
|
85
|
+
if (mode === "expanded") return markdown;
|
|
86
|
+
|
|
87
|
+
const text = markdown.trim();
|
|
88
|
+
if (!text) return markdown;
|
|
89
|
+
|
|
90
|
+
if (mode === "collapsed") return collapseLine(text);
|
|
91
|
+
|
|
92
|
+
const steps = splitThinkingIntoStepTexts(text);
|
|
93
|
+
if (steps.length === 0) return collapseLine(text);
|
|
94
|
+
|
|
95
|
+
const visible = steps.slice(0, SUMMARY_LIST_LIMIT);
|
|
96
|
+
const lines = visible.map((step) => {
|
|
97
|
+
const core = deriveStepCore(step);
|
|
98
|
+
return `- ${core.icon} ${core.summary}`;
|
|
99
|
+
});
|
|
100
|
+
if (steps.length > visible.length) {
|
|
101
|
+
lines.push(`- … (+${steps.length - visible.length} more)`);
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyeeme/pi-thinking-ui",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Faithful, terminal-native three-mode thinking visualization for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,10 +28,9 @@
|
|
|
28
28
|
],
|
|
29
29
|
"files": [
|
|
30
30
|
"index.ts",
|
|
31
|
-
"
|
|
31
|
+
"markdown-render.ts",
|
|
32
32
|
"parse.ts",
|
|
33
33
|
"persistence.ts",
|
|
34
|
-
"render.ts",
|
|
35
34
|
"state.ts",
|
|
36
35
|
"types.ts",
|
|
37
36
|
"README.md",
|
|
@@ -48,14 +47,14 @@
|
|
|
48
47
|
"typecheck": "tsc"
|
|
49
48
|
},
|
|
50
49
|
"peerDependencies": {
|
|
51
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
52
|
-
"@earendil-works/pi-ai": ">=0.
|
|
53
|
-
"@earendil-works/pi-tui": ">=0.
|
|
50
|
+
"@earendil-works/pi-coding-agent": ">=0.84.1",
|
|
51
|
+
"@earendil-works/pi-ai": ">=0.84.1",
|
|
52
|
+
"@earendil-works/pi-tui": ">=0.84.1"
|
|
54
53
|
},
|
|
55
54
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
57
|
-
"@earendil-works/pi-ai": "0.
|
|
58
|
-
"@earendil-works/pi-tui": "0.
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
56
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
57
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
59
58
|
"@types/node": "22.19.19",
|
|
60
59
|
"typescript": "5.9.3"
|
|
61
60
|
}
|