pi-minimalist 0.3.1 → 0.4.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/index.ts CHANGED
@@ -34,7 +34,11 @@ export default function (pi: ExtensionAPI) {
34
34
  // strand every row on stale state.
35
35
  const { config, grouping } = sharedState(loadSettings());
36
36
 
37
- installBridges({ config, grouping });
37
+ // A timer (the post-stream thinking linger) must repaint outside any render.
38
+ // Extensions get no TUI handle except through a factory, so an invisible
39
+ // widget (zero lines, below the editor, no spacer) captures it.
40
+ let tui: { requestRender(): void } | undefined;
41
+ installBridges({ config, grouping, requestRender: () => tui?.requestRender() });
38
42
  // Idempotent, so a /reload only refreshes the Bridge the wrappers read.
39
43
  patchCore(core);
40
44
 
@@ -46,7 +50,19 @@ export default function (pi: ExtensionAPI) {
46
50
  // restart. Session overrides are re-applied by Config.replace().
47
51
  config.replace(loadSettings());
48
52
  };
49
- pi.on("session_start", refresh);
53
+ pi.on("session_start", (_event, ctx) => {
54
+ refresh();
55
+ if (ctx.hasUI && ctx.mode === "tui") {
56
+ ctx.ui.setWidget(
57
+ "pi-minimalist-repaint",
58
+ (captured) => {
59
+ tui = captured;
60
+ return { render: () => [], invalidate() {} };
61
+ },
62
+ { placement: "belowEditor" },
63
+ );
64
+ }
65
+ });
50
66
  pi.on("turn_start", refresh);
51
67
  pi.on("agent_start", () => grouping.agentStarted());
52
68
  pi.on("agent_settled", () => grouping.agentSettled());
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "pi-minimalist",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Compact one-line tool and thinking rows for Pi's interactive transcript",
5
- "keywords": ["pi-package"],
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
6
8
  "license": "MIT",
7
9
  "author": "Pinglei He",
8
10
  "repository": {
@@ -10,13 +12,20 @@
10
12
  "url": "git+https://github.com/EviHex/pi-minimalist.git"
11
13
  },
12
14
  "type": "module",
13
- "files": ["index.ts", "src/", "README.md", "LICENSE"],
15
+ "files": [
16
+ "index.ts",
17
+ "src/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
14
21
  "peerDependencies": {
15
22
  "@earendil-works/pi-coding-agent": "*",
16
23
  "@earendil-works/pi-tui": "*"
17
24
  },
18
25
  "pi": {
19
- "extensions": ["./index.ts"],
26
+ "extensions": [
27
+ "./index.ts"
28
+ ],
20
29
  "image": "https://raw.githubusercontent.com/EviHex/pi-minimalist/main/docs/click.png"
21
30
  }
22
31
  }
package/src/bridge.ts CHANGED
@@ -16,7 +16,7 @@ import { RunGrouping } from "./run-grouping.ts";
16
16
  import { Painter, type Row, type ThemeLike } from "./row.ts";
17
17
  import { createToolRenderer, type ToolRenderer } from "./tool-renderer.ts";
18
18
  import { createThinkingPreview, singleHueThinkingTheme, type ThinkingPreview } from "./thinking.ts";
19
- import type { Timers } from "./components.ts";
19
+ import { realTimers, type Timers } from "./components.ts";
20
20
 
21
21
  export const BRIDGE_SYMBOLS = {
22
22
  /** The `Bridge` object: everything the prototype wrappers call. */
@@ -42,6 +42,11 @@ export type Bridge = {
42
42
  compactThinking: () => boolean;
43
43
  /** Keep a STREAMING thinking block expanded. */
44
44
  keepActiveThinkingExpanded: () => boolean;
45
+ /**
46
+ * Run `callback` once, `THINKING_LINGER_MS` after a streamed message ends,
47
+ * then repaint. Keeps a short thought on screen long enough to be read.
48
+ */
49
+ afterThinkingLinger: (callback: () => void) => void;
45
50
 
46
51
  /** Chronology hooks that keep thinking and prose in transcript order. */
47
52
  observeThinking: (owner: object, runIndex: number, streaming: boolean, hidden: boolean, timestamp?: number) => string;
@@ -103,8 +108,13 @@ export function sharedState(settings?: ConstructorParameters<typeof Config>[0]):
103
108
  export type InstallOptions = SharedState & {
104
109
  /** Injectable repaint scheduler; production uses Node's timers. */
105
110
  timers?: Timers;
111
+ /** Repaint the TUI from outside a render (a timer). No-op without a TUI. */
112
+ requestRender?: () => void;
106
113
  };
107
114
 
115
+ /** How long expanded thinking stays open after its message stops streaming. */
116
+ export const THINKING_LINGER_MS = 3000;
117
+
108
118
  /**
109
119
  * Install the bridge.
110
120
  *
@@ -112,7 +122,7 @@ export type InstallOptions = SharedState & {
112
122
  * from `session_shutdown`: reload ordering can let an old shutdown hook erase the
113
123
  * newly installed bridge. Process exit clears globalThis naturally.
114
124
  */
115
- export function installBridges({ config, grouping, timers }: InstallOptions): void {
125
+ export function installBridges({ config, grouping, timers = realTimers, requestRender }: InstallOptions): void {
116
126
  const globals = globalThis as Globals;
117
127
  const painter = (theme: ThemeLike) => new Painter(theme, config);
118
128
 
@@ -122,6 +132,14 @@ export function installBridges({ config, grouping, timers }: InstallOptions): vo
122
132
  thinkingMarkdownTheme: (base, theme) => singleHueThinkingTheme(base, theme, config),
123
133
  compactThinking: () => config.get("thinkingAsToolCall") === true,
124
134
  keepActiveThinkingExpanded: () => config.get("keepActiveThinkingExpanded"),
135
+ afterThinkingLinger: (callback) => {
136
+ // Timers only has intervals; clearing on the first tick makes it one-shot.
137
+ const handle = timers.setInterval(() => {
138
+ timers.clearInterval(handle);
139
+ callback();
140
+ requestRender?.();
141
+ }, THINKING_LINGER_MS);
142
+ },
125
143
 
126
144
  // Chronology: core walks message content in transcript order, and run
127
145
  // folding depends on that order.
package/src/config-ui.ts CHANGED
@@ -71,7 +71,7 @@ export const FIELDS: Field[] = [
71
71
  {
72
72
  key: "keepActiveThinkingExpanded",
73
73
  label: "Show thinking while generating",
74
- description: "Show the full thinking block as it arrives, instead of a one-line preview.",
74
+ description: "Show the full thinking block as it arrives (and for 3s after), instead of a one-line preview.",
75
75
  },
76
76
  {
77
77
  key: "glyphStyle",
package/src/core-patch.ts CHANGED
@@ -264,7 +264,10 @@ function isMouseRegion(value: any): boolean {
264
264
  * `hideThinkingBlock` + the override map, then restoring both. Either way the
265
265
  * click handler stores into the LIVE map, so an explicit Ctrl+T expansion
266
266
  * always survives — the default only decides what happens when the user has
267
- * not expressed a preference.
267
+ * not expressed a preference. When a streamed message ends, the forced
268
+ * expansion LINGERS for `THINKING_LINGER_MS`, then the message re-renders
269
+ * collapsed: a one-phrase thought would otherwise vanish before it is read.
270
+ * Replayed history never streamed, so it never lingers.
268
271
  * - collapsed preview: swap the hidden run's `Text` for our compact line.
269
272
  * - expanded theme: recolor the run's `Markdown` in place (its private `theme`
270
273
  * field is read at render time, and this runs before the first render).
@@ -283,13 +286,26 @@ export function patchAssistantMessage(proto: AssistantProto): void {
283
286
  // its mouse layout, so Pi's y (relative to the lines we returned) would
284
287
  // miss the summary by that shift. Remember it to translate clicks back.
285
288
  const mouseShift = new WeakMap<AssistantProto, { shift: number; height: number }>();
289
+ // Messages seen streaming, and those still inside their post-stream linger.
290
+ const streamed = new WeakSet<AssistantProto>();
291
+ const lingering = new WeakSet<AssistantProto>();
286
292
 
287
293
  proto.updateContent = function (this: AssistantProto, message: any, isStreaming = this.isStreaming) {
288
294
  const savedHide = this.hideThinkingBlock;
289
295
  const savedOverrides = this.thinkingVisibilityOverrides;
296
+ const b = bridge();
297
+ const keepOpen = b?.keepActiveThinkingExpanded() === true;
298
+ if (isStreaming) streamed.add(this);
299
+ else if (streamed.delete(this) && keepOpen && b) {
300
+ lingering.add(this);
301
+ b.afterThinkingLinger(() => {
302
+ lingering.delete(this);
303
+ if (this.lastMessage) this.updateContent(this.lastMessage, false);
304
+ });
305
+ }
290
306
  // Default: a streaming block stays collapsed to its compact preview, which
291
307
  // already shows the newest text. Opt in to force it open instead.
292
- const forceOpen = isStreaming && bridge()?.keepActiveThinkingExpanded() === true;
308
+ const forceOpen = keepOpen && (isStreaming || lingering.has(this));
293
309
  if (forceOpen) {
294
310
  this.hideThinkingBlock = false;
295
311
  this.thinkingVisibilityOverrides = new Map();