@fyeeme/pi-thinking-ui 1.0.0 → 1.0.1

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 CHANGED
@@ -1,6 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
- ### Added
3
+ ## [1.0.1] - 2025-07-25
4
+
5
+ ### Changed
6
+ - Default thinking view mode is now `collapsed` instead of `summary`
7
+ - Step-core cache: completed step summaries are memoized by step text, avoiding re-summarization of unchanged steps during streaming (linearizes per-derive summarization cost)
8
+ - Inlined `blocksLengthFingerprint` at its single call site
9
+
10
+ ### Fixed
11
+ - Respect the host `hideThinkingBlock` setting: when thinking is hidden, the extension falls back to Pi's native hidden-label renderer instead of forcing the custom thinking UI
4
12
  - Three-mode thinking visualization: `collapsed`, `summary`, `expanded`
5
13
  - Deterministic step derivation from raw thinking text
6
14
  - Semantic role inference with icons and colors
package/index.ts CHANGED
@@ -63,7 +63,7 @@ async function restoreMode(ctx: ExtensionContext): Promise<ThinkingUIMode> {
63
63
  if (projectMode) return projectMode;
64
64
 
65
65
  const globalMode = await readRestoredModePreference(ctx, "global");
66
- return globalMode ?? "summary";
66
+ return globalMode ?? "collapsed";
67
67
  }
68
68
 
69
69
  function refreshThinkingUI(ctx: ExtensionContext): void {
package/internal-patch.ts CHANGED
@@ -271,6 +271,10 @@ async function installPatch(): Promise<() => void> {
271
271
 
272
272
  const patchedUpdateContent = function patchedUpdateContent(this: AssistantMessageComponentPrototype, message: AssistantMessage): void {
273
273
  this.lastMessage = message;
274
+ if (this.hideThinkingBlock) {
275
+ fallbackToOriginalUpdateContent(this, message, "updateContent");
276
+ return;
277
+ }
274
278
  if (!hasPatchableContentContainer(this)) {
275
279
  fallbackToOriginalUpdateContent(this, message, "updateContent");
276
280
  return;
@@ -333,7 +337,7 @@ async function installPatch(): Promise<() => void> {
333
337
  return;
334
338
  }
335
339
 
336
- this.hideThinkingBlock = false;
340
+ this.hideThinkingBlock = hide;
337
341
  if (!this.lastMessage) return;
338
342
  try {
339
343
  this.updateContent(this.lastMessage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyeeme/pi-thinking-ui",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Faithful, terminal-native three-mode thinking visualization for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/parse.ts CHANGED
@@ -1086,7 +1086,38 @@ export function iconForThinkingRole(role: ThinkingSemanticRole): string {
1086
1086
  }
1087
1087
  }
1088
1088
 
1089
- export function deriveThinkingUI(blocks: ThinkingSourceBlock[]): DerivedThinkingStep[] {
1089
+ export interface DerivedStepCore {
1090
+ summary: string;
1091
+ baselineSummary: string;
1092
+ challengerSummary: string;
1093
+ summaryEvents: ThinkingSummaryEvent[];
1094
+ collapsedPriority: number;
1095
+ hasExplicitFailure: boolean;
1096
+ hasExplicitSuccess: boolean;
1097
+ role: ThinkingSemanticRole;
1098
+ icon: string;
1099
+ }
1100
+
1101
+ export function deriveStepCore(stepText: string): DerivedStepCore {
1102
+ const summaryDetails = summarizeThinkingTextDetailed(stepText);
1103
+ const role = inferThinkingRole(`${summaryDetails.summary}\n${stepText}`);
1104
+ return {
1105
+ summary: summaryDetails.summary,
1106
+ baselineSummary: summaryDetails.baselineSummary,
1107
+ challengerSummary: summaryDetails.challengerSummary,
1108
+ summaryEvents: summaryDetails.events,
1109
+ collapsedPriority: summaryDetails.collapsedPriority,
1110
+ hasExplicitFailure: summaryDetails.hasExplicitFailure,
1111
+ hasExplicitSuccess: summaryDetails.hasExplicitSuccess,
1112
+ role,
1113
+ icon: iconForThinkingRole(role),
1114
+ };
1115
+ }
1116
+
1117
+ export function deriveThinkingUI(
1118
+ blocks: ThinkingSourceBlock[],
1119
+ resolveCore: (stepText: string) => DerivedStepCore = deriveStepCore,
1120
+ ): DerivedThinkingStep[] {
1090
1121
  const steps: DerivedThinkingStep[] = [];
1091
1122
  blocks.forEach((block, blockIndex) => {
1092
1123
  if (block.redacted && !block.text.trim()) {
@@ -1112,23 +1143,22 @@ export function deriveThinkingUI(blocks: ThinkingSourceBlock[]): DerivedThinking
1112
1143
 
1113
1144
  const stepTexts = splitThinkingIntoStepTexts(block.text);
1114
1145
  stepTexts.forEach((stepText, stepIndex) => {
1115
- const summaryDetails = summarizeThinkingTextDetailed(stepText);
1116
- const role = inferThinkingRole(`${summaryDetails.summary}\n${stepText}`);
1146
+ const core = resolveCore(stepText);
1117
1147
  steps.push({
1118
1148
  id: `${block.contentIndex}-${stepIndex}`,
1119
1149
  contentIndex: block.contentIndex,
1120
1150
  blockIndex,
1121
1151
  stepIndex,
1122
- summary: summaryDetails.summary,
1152
+ summary: core.summary,
1123
1153
  body: stepText.trim(),
1124
- role,
1125
- icon: iconForThinkingRole(role),
1126
- baselineSummary: summaryDetails.baselineSummary,
1127
- challengerSummary: summaryDetails.challengerSummary,
1128
- summaryEvents: summaryDetails.events,
1129
- collapsedPriority: summaryDetails.collapsedPriority,
1130
- hasExplicitFailure: summaryDetails.hasExplicitFailure,
1131
- hasExplicitSuccess: summaryDetails.hasExplicitSuccess,
1154
+ role: core.role,
1155
+ icon: core.icon,
1156
+ baselineSummary: core.baselineSummary,
1157
+ challengerSummary: core.challengerSummary,
1158
+ summaryEvents: core.summaryEvents,
1159
+ collapsedPriority: core.collapsedPriority,
1160
+ hasExplicitFailure: core.hasExplicitFailure,
1161
+ hasExplicitSuccess: core.hasExplicitSuccess,
1132
1162
  });
1133
1163
  });
1134
1164
  });
package/render.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Component } from "@earendil-works/pi-tui";
2
2
  import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
- import { deriveThinkingUI } from "./parse.ts";
3
+ import { deriveStepCore, deriveThinkingUI } from "./parse.ts";
4
+ import type { DerivedStepCore } from "./parse.ts";
4
5
  import { getActiveThinkingState, getCurrentThinkingScopeKey, getThinkingUIMode } from "./state.ts";
5
6
  import type { DerivedThinkingStep, ThinkingSemanticRole, ThinkingSourceBlock, ThinkingThemeLike } from "./types.ts";
6
7
 
@@ -357,8 +358,81 @@ export function renderThinkingUILines(theme: ThinkingThemeLike, width: number, o
357
358
  return renderSummary(theme, width, options.steps, options.activeStepId);
358
359
  }
359
360
 
361
+ // Throttle: during active streaming, skip re-derivation unless the total
362
+ // character change across blocks exceeds this threshold. This keeps the
363
+ // spinner and input responsive during deep thinking while still updating
364
+ // the summary at a visible cadence.
365
+ const MIN_DELTA_CHARS_FOR_REDERIVE = 200;
366
+ const THROTTLE_CACHE_LIMIT = 4;
367
+ const throttleCache = new Map<number, { lengths: number[]; steps: DerivedThinkingStep[] }>();
368
+
369
+ // Step-core cache: thinking text is append-only, so completed step texts are
370
+ // byte-stable across re-derivations. Caching the expensive core (summary +
371
+ // role) by step text avoids re-summarizing N-1 unchanged steps on each
372
+ // throttle-miss derive, linearizing the per-derive summarization cost.
373
+ const STEP_CORE_CACHE_LIMIT = 500;
374
+ const stepCoreCache = new Map<string, DerivedStepCore>();
375
+
376
+ function cachingCoreResolver(stepText: string): DerivedStepCore {
377
+ const cached = stepCoreCache.get(stepText);
378
+ if (cached) {
379
+ stepCoreCache.delete(stepText);
380
+ stepCoreCache.set(stepText, cached);
381
+ return cached;
382
+ }
383
+ const core = deriveStepCore(stepText);
384
+ if (stepCoreCache.size >= STEP_CORE_CACHE_LIMIT) {
385
+ const oldestKey = stepCoreCache.keys().next().value;
386
+ if (oldestKey !== undefined) {
387
+ stepCoreCache.delete(oldestKey);
388
+ }
389
+ }
390
+ stepCoreCache.set(stepText, core);
391
+ return core;
392
+ }
393
+
394
+ function totalLengthDelta(current: number[], previous: number[]): number {
395
+ let delta = 0;
396
+ for (let index = 0; index < Math.max(current.length, previous.length); index += 1) {
397
+ delta += Math.abs((current[index] ?? 0) - (previous[index] ?? 0));
398
+ }
399
+ return delta;
400
+ }
401
+
402
+ function deriveOrReuseSteps(
403
+ messageTimestamp: number,
404
+ scopeKey: string,
405
+ blocks: ThinkingSourceBlock[],
406
+ ): DerivedThinkingStep[] {
407
+ const active = getActiveThinkingState(messageTimestamp, scopeKey);
408
+ const lengths = blocks.map((block) => block.text.length);
409
+ const cached = throttleCache.get(messageTimestamp);
410
+
411
+ // During active streaming, skip re-derivation when only a few chars were appended.
412
+ if (active.active && cached) {
413
+ const delta = totalLengthDelta(lengths, cached.lengths);
414
+ if (delta < MIN_DELTA_CHARS_FOR_REDERIVE) {
415
+ return cached.steps;
416
+ }
417
+ }
418
+
419
+ const steps = deriveThinkingUI(blocks, cachingCoreResolver);
420
+
421
+ if (active.active) {
422
+ if (throttleCache.size >= THROTTLE_CACHE_LIMIT && !throttleCache.has(messageTimestamp)) {
423
+ const oldestKey = throttleCache.keys().next().value;
424
+ if (oldestKey !== undefined) {
425
+ throttleCache.delete(oldestKey);
426
+ }
427
+ }
428
+ throttleCache.set(messageTimestamp, { lengths, steps });
429
+ }
430
+
431
+ return steps;
432
+ }
433
+
360
434
  export class ThinkingUIComponent implements Component {
361
- private steps: DerivedThinkingStep[];
435
+ private readonly steps: DerivedThinkingStep[];
362
436
  private cacheKey?: string;
363
437
  private cachedLines?: string[];
364
438
  private readonly scopeKey: string;
@@ -373,8 +447,8 @@ export class ThinkingUIComponent implements Component {
373
447
  ) {
374
448
  this.theme = theme;
375
449
  this.messageTimestamp = messageTimestamp;
376
- this.steps = deriveThinkingUI(blocks);
377
450
  this.scopeKey = scopeKey ?? getCurrentThinkingScopeKey();
451
+ this.steps = deriveOrReuseSteps(messageTimestamp, this.scopeKey, blocks);
378
452
  }
379
453
 
380
454
  render(width: number): string[] {
package/state.ts CHANGED
@@ -54,7 +54,7 @@ function normalizeThinkingScopeKey(scopeKey?: string): string {
54
54
  }
55
55
 
56
56
  function normalizeThinkingMode(mode: unknown): ThinkingUIMode {
57
- return mode === "collapsed" || mode === "summary" || mode === "expanded" ? mode : "summary";
57
+ return mode === "collapsed" || mode === "summary" || mode === "expanded" ? mode : "collapsed";
58
58
  }
59
59
 
60
60
  function normalizeActiveThinkingState(value: unknown): ActiveThinkingState {
@@ -167,7 +167,7 @@ const globalState = (() => {
167
167
  }
168
168
  const created: ThinkingUIGlobalState = {
169
169
  currentScopeKey: DEFAULT_SCOPE_KEY,
170
- modeByScopeKey: { [DEFAULT_SCOPE_KEY]: "summary" },
170
+ modeByScopeKey: { [DEFAULT_SCOPE_KEY]: "collapsed" },
171
171
  activeByScopeKey: { [DEFAULT_SCOPE_KEY]: {} },
172
172
  lastActiveByScopeKey: { [DEFAULT_SCOPE_KEY]: { active: false } },
173
173
  refreshToggleByScope: {},
@@ -183,7 +183,7 @@ const globalState = (() => {
183
183
 
184
184
  function ensureScopeState(scopeKey: string): void {
185
185
  if (!(scopeKey in globalState.modeByScopeKey)) {
186
- globalState.modeByScopeKey[scopeKey] = "summary";
186
+ globalState.modeByScopeKey[scopeKey] = "collapsed";
187
187
  }
188
188
  if (!(scopeKey in globalState.activeByScopeKey)) {
189
189
  globalState.activeByScopeKey[scopeKey] = {};
@@ -215,7 +215,7 @@ export function setCurrentThinkingScopeKey(scopeKey: string): void {
215
215
  export function getThinkingUIMode(scopeKey?: string): ThinkingUIMode {
216
216
  const normalizedScopeKey = normalizeThinkingScopeKey(scopeKey ?? globalState.currentScopeKey);
217
217
  ensureScopeState(normalizedScopeKey);
218
- return globalState.modeByScopeKey[normalizedScopeKey] ?? "summary";
218
+ return globalState.modeByScopeKey[normalizedScopeKey] ?? "collapsed";
219
219
  }
220
220
 
221
221
  export function setThinkingUIMode(mode: ThinkingUIMode, scopeKey?: string): void {
@@ -370,7 +370,7 @@ export function resetThinkingUIViewState(scopeKey?: string): void {
370
370
  if (scopeKey !== undefined) {
371
371
  const normalizedScopeKey = normalizeThinkingScopeKey(scopeKey);
372
372
  globalState.currentScopeKey = normalizedScopeKey;
373
- globalState.modeByScopeKey[normalizedScopeKey] = "summary";
373
+ globalState.modeByScopeKey[normalizedScopeKey] = "collapsed";
374
374
  globalState.refreshToggleByScope[normalizedScopeKey] = false;
375
375
  globalState.activeByScopeKey[normalizedScopeKey] = {};
376
376
  globalState.lastActiveByScopeKey[normalizedScopeKey] = { active: false };
@@ -379,7 +379,7 @@ export function resetThinkingUIViewState(scopeKey?: string): void {
379
379
  }
380
380
 
381
381
  globalState.currentScopeKey = DEFAULT_SCOPE_KEY;
382
- globalState.modeByScopeKey = { [DEFAULT_SCOPE_KEY]: "summary" };
382
+ globalState.modeByScopeKey = { [DEFAULT_SCOPE_KEY]: "collapsed" };
383
383
  globalState.activeByScopeKey = { [DEFAULT_SCOPE_KEY]: {} };
384
384
  globalState.lastActiveByScopeKey = { [DEFAULT_SCOPE_KEY]: { active: false } };
385
385
  globalState.refreshToggleByScope = {};