@quandev104/pi-style 0.1.6 → 0.1.7

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.
@@ -48,12 +48,14 @@ const allowedPaths = new Set([
48
48
  "messages.assistantPrefix",
49
49
  "messages.specialBlocks",
50
50
  "messages.hideThinkingLabel",
51
+ "messages.hideInterimText",
51
52
  "tools.enabled",
52
53
  "tools.style",
53
54
  "tools.maxCollapsedLines",
54
55
  "tools.maxExpandedLines",
55
56
  "tools.dimOutput",
56
57
  "tools.showElapsed",
58
+ "tools.collapseAfterTurn",
57
59
  "theme.nerdFonts",
58
60
  "theme.terminalBackgroundSync",
59
61
  "theme.autoApply",
@@ -78,9 +80,11 @@ function validatePathValue(path: string, value: unknown): boolean {
78
80
  "messages.assistantPrefix",
79
81
  "messages.specialBlocks",
80
82
  "messages.hideThinkingLabel",
83
+ "messages.hideInterimText",
81
84
  "tools.enabled",
82
85
  "tools.showElapsed",
83
86
  "tools.dimOutput",
87
+ "tools.collapseAfterTurn",
84
88
  "compatibility.allowSafePatches",
85
89
  "compatibility.allowCorePatches",
86
90
  "compatibility.preferExistingEditor",
@@ -28,7 +28,13 @@ export const DEFAULT_CONFIG: NormalizedPiStyleConfig = Object.freeze({
28
28
  contextBarWidth: 10,
29
29
  }),
30
30
  editor: Object.freeze({ enabled: true, style: "dock", frame: "rounded", showMetadata: false, hint: "" }),
31
- messages: Object.freeze({ enabled: true, assistantPrefix: true, specialBlocks: true, hideThinkingLabel: true }),
31
+ messages: Object.freeze({
32
+ enabled: true,
33
+ assistantPrefix: true,
34
+ specialBlocks: true,
35
+ hideThinkingLabel: true,
36
+ hideInterimText: true,
37
+ }),
32
38
  tools: Object.freeze({
33
39
  enabled: true,
34
40
  style: "compact-box",
@@ -36,6 +42,7 @@ export const DEFAULT_CONFIG: NormalizedPiStyleConfig = Object.freeze({
36
42
  maxExpandedLines: 50,
37
43
  dimOutput: false,
38
44
  showElapsed: true,
45
+ collapseAfterTurn: true,
39
46
  }),
40
47
  theme: Object.freeze({
41
48
  nerdFonts: "auto",
@@ -169,6 +176,7 @@ export function normalizeConfig(
169
176
  assistantPrefix: bool(messages.assistantPrefix, defaults.messages.assistantPrefix),
170
177
  specialBlocks: bool(messages.specialBlocks, defaults.messages.specialBlocks),
171
178
  hideThinkingLabel: bool(messages.hideThinkingLabel, defaults.messages.hideThinkingLabel),
179
+ hideInterimText: bool(messages.hideInterimText, defaults.messages.hideInterimText),
172
180
  }),
173
181
  tools: Object.freeze({
174
182
  enabled: bool(tools.enabled, defaults.tools.enabled),
@@ -177,6 +185,7 @@ export function normalizeConfig(
177
185
  maxExpandedLines: maxExpanded,
178
186
  dimOutput: bool(tools.dimOutput, defaults.tools.dimOutput),
179
187
  showElapsed: bool(tools.showElapsed, defaults.tools.showElapsed),
188
+ collapseAfterTurn: bool(tools.collapseAfterTurn, defaults.tools.collapseAfterTurn),
180
189
  }),
181
190
  theme: Object.freeze({
182
191
  nerdFonts: stringEnum(theme.nerdFonts, ["auto", "on", "off"], defaults.theme.nerdFonts),
@@ -227,9 +236,11 @@ const BOOL_PATHS = new Set([
227
236
  "messages.assistantPrefix",
228
237
  "messages.specialBlocks",
229
238
  "messages.hideThinkingLabel",
239
+ "messages.hideInterimText",
230
240
  "tools.enabled",
231
241
  "tools.showElapsed",
232
242
  "tools.dimOutput",
243
+ "tools.collapseAfterTurn",
233
244
  "compatibility.allowSafePatches",
234
245
  "compatibility.allowCorePatches",
235
246
  "compatibility.preferExistingEditor",
@@ -10,7 +10,7 @@ export const CONFIG_PRESETS: Readonly<Record<PresetName, Readonly<PiStyleConfig>
10
10
  editor: { style: "native", frame: "native", showMetadata: false },
11
11
  startup: { mode: "off" },
12
12
  messages: { enabled: false },
13
- tools: { enabled: false },
13
+ tools: { enabled: false, collapseAfterTurn: false },
14
14
  theme: { terminalBackgroundSync: "off" },
15
15
  }),
16
16
  compact: Object.freeze({
@@ -41,7 +41,7 @@ export const CONFIG_PRESETS: Readonly<Record<PresetName, Readonly<PiStyleConfig>
41
41
  editor: { style: "native", frame: "native", showMetadata: false },
42
42
  startup: { mode: "off" },
43
43
  messages: { enabled: false },
44
- tools: { enabled: false },
44
+ tools: { enabled: false, collapseAfterTurn: false },
45
45
  theme: { terminalBackgroundSync: "off", autoApply: "off" },
46
46
  }),
47
47
  });
@@ -32,7 +32,13 @@ export interface PiStyleConfig {
32
32
  contextBarWidth?: number;
33
33
  };
34
34
  editor?: { enabled?: boolean; style?: string; frame?: string; showMetadata?: boolean; hint?: string };
35
- messages?: { enabled?: boolean; assistantPrefix?: boolean; specialBlocks?: boolean; hideThinkingLabel?: boolean };
35
+ messages?: {
36
+ enabled?: boolean;
37
+ assistantPrefix?: boolean;
38
+ specialBlocks?: boolean;
39
+ hideThinkingLabel?: boolean;
40
+ hideInterimText?: boolean;
41
+ };
36
42
  tools?: {
37
43
  enabled?: boolean;
38
44
  style?: string;
@@ -40,6 +46,8 @@ export interface PiStyleConfig {
40
46
  maxExpandedLines?: number;
41
47
  dimOutput?: boolean;
42
48
  showElapsed?: boolean;
49
+ /** Collapse a completed turn's tool blocks into one summary line (ADR 0007). */
50
+ collapseAfterTurn?: boolean;
43
51
  };
44
52
  theme?: {
45
53
  nerdFonts?: string;
@@ -81,7 +89,13 @@ export interface NormalizedPiStyleConfig {
81
89
  contextBarWidth: number;
82
90
  };
83
91
  readonly editor: { enabled: boolean; style: EditorStyle; frame: EditorFrame; showMetadata: boolean; hint: string };
84
- readonly messages: { enabled: boolean; assistantPrefix: boolean; specialBlocks: boolean; hideThinkingLabel: boolean };
92
+ readonly messages: {
93
+ enabled: boolean;
94
+ assistantPrefix: boolean;
95
+ specialBlocks: boolean;
96
+ hideThinkingLabel: boolean;
97
+ hideInterimText: boolean;
98
+ };
85
99
  readonly tools: {
86
100
  enabled: boolean;
87
101
  style: string;
@@ -89,6 +103,8 @@ export interface NormalizedPiStyleConfig {
89
103
  maxExpandedLines: number;
90
104
  dimOutput: boolean;
91
105
  showElapsed: boolean;
106
+ /** Collapse a completed turn's tool blocks into one summary line (ADR 0007). */
107
+ collapseAfterTurn: boolean;
92
108
  };
93
109
  readonly theme: {
94
110
  nerdFonts: NerdFontsMode;
@@ -178,6 +178,8 @@ export type MessageDecorationSnapshot = Readonly<{
178
178
  assistantEnabled: boolean;
179
179
  /** Drop the hidden-thinking label row and its trailing spacer (zero-trace collapse). */
180
180
  collapseHiddenThinking: boolean;
181
+ /** Hide the text of assistant messages that also carry tool calls (interim narration). */
182
+ hideInterimText: boolean;
181
183
  }>;
182
184
 
183
185
  export function decorateMessageRender(
@@ -188,6 +190,7 @@ export function decorateMessageRender(
188
190
  assistantPrefix: "│ ",
189
191
  assistantEnabled: true,
190
192
  collapseHiddenThinking: false,
193
+ hideInterimText: false,
191
194
  },
192
195
  ): unknown {
193
196
  if (typeof original !== "function") return undefined;
@@ -207,6 +210,34 @@ function isSpacerChild(child: unknown): boolean {
207
210
  return typeof (child as { setLines?: unknown } | undefined)?.setLines === "function";
208
211
  }
209
212
 
213
+ /**
214
+ * The assistant TEXT Markdown block: `Markdown(text, pad, 0, theme, undefined,
215
+ * { transform })` — duck-typed by setText + options object + no defaultTextStyle.
216
+ * Thinking Markdown passes a `{ color, italic }` defaultTextStyle and is
217
+ * therefore excluded; plain Text components carry no `options`.
218
+ */
219
+ function isInterimTextChild(child: unknown): boolean {
220
+ const candidate = child as { setText?: unknown; options?: unknown; defaultTextStyle?: unknown } | undefined;
221
+ return (
222
+ typeof candidate?.setText === "function" &&
223
+ candidate.options !== undefined &&
224
+ typeof candidate.options === "object" &&
225
+ candidate.defaultTextStyle === undefined
226
+ );
227
+ }
228
+
229
+ /** Whether the message content includes a toolCall item (interim narration marker). */
230
+ function hasToolCallItems(message: unknown): boolean {
231
+ if (!message || typeof message !== "object") return false;
232
+ const content = (message as { content?: unknown }).content;
233
+ return (
234
+ Array.isArray(content) &&
235
+ content.some(
236
+ (item) => item !== null && typeof item === "object" && (item as { type?: unknown }).type === "toolCall",
237
+ )
238
+ );
239
+ }
240
+
210
241
  /**
211
242
  * The hidden-thinking placeholder: a Text (setCustomBgFn) whose ANSI-stripped
212
243
  * rendered content is empty. Duck-typed on the public shape because
@@ -240,11 +271,11 @@ export function decorateMessageUpdate(
240
271
  assistantPrefix: "│ ",
241
272
  assistantEnabled: true,
242
273
  collapseHiddenThinking: false,
274
+ hideInterimText: false,
243
275
  },
244
276
  ): unknown {
245
277
  if (typeof original !== "function") return undefined;
246
278
  const result = Reflect.apply(original, instance, args);
247
- if (!snapshot.collapseHiddenThinking) return result;
248
279
  const target = instance as {
249
280
  hideThinkingBlock?: boolean;
250
281
  hiddenThinkingLabel?: string;
@@ -252,15 +283,32 @@ export function decorateMessageUpdate(
252
283
  };
253
284
  // Only meaningful when Pi renders the hidden-block label (hideThinkingBlock)
254
285
  // and the extension has blanked that label out ("" — the zero-trace mode).
255
- if (target.hideThinkingBlock !== true || target.hiddenThinkingLabel !== "") return result;
256
- const children = target.contentContainer?.children;
257
- if (!children) return result;
258
- for (let index = children.length - 1; index >= 0; index--) {
259
- if (!isBlankTextChild(children[index])) continue;
260
- children.splice(index, 1);
261
- // Drop the Spacer(1) the native layout appends after the thinking run when
262
- // another visible block follows; the message keeps only its shared top padding.
263
- if (isSpacerChild(children[index])) children.splice(index, 1);
286
+ if (snapshot.collapseHiddenThinking && target.hideThinkingBlock === true && target.hiddenThinkingLabel === "") {
287
+ const children = target.contentContainer?.children;
288
+ if (children) {
289
+ for (let index = children.length - 1; index >= 0; index--) {
290
+ if (!isBlankTextChild(children[index])) continue;
291
+ children.splice(index, 1);
292
+ // Drop the Spacer(1) the native layout appends after the thinking run when
293
+ // another visible block follows; the message keeps only its shared top padding.
294
+ if (isSpacerChild(children[index])) children.splice(index, 1);
295
+ }
296
+ }
297
+ }
298
+ // Interim narration: assistant messages that carry tool calls use their text
299
+ // only to narrate while working; the tool blocks tell the story. Hide the text
300
+ // so the feed shows the run's summary and the final answer. Deterministic per
301
+ // content — streaming, scroll-back, and resume behave identically. Errors and
302
+ // truncation notices are Text children and stay; if only spacers remain the
303
+ // message becomes zero-trace.
304
+ if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
305
+ const children = target.contentContainer?.children;
306
+ if (children) {
307
+ for (let index = children.length - 1; index >= 0; index--) {
308
+ if (isInterimTextChild(children[index])) children.splice(index, 1);
309
+ }
310
+ if (children.every((child) => isSpacerChild(child))) children.length = 0;
311
+ }
264
312
  }
265
313
  return result;
266
314
  }
@@ -7,7 +7,7 @@
7
7
  import type { Component } from "@earendil-works/pi-tui";
8
8
  import type { BoxTheme } from "../../../shared/box.js";
9
9
  import { bashTool } from "./bash.js";
10
- import { closeActiveBatch, isBatchableTool } from "./batch.js";
10
+ import { closeActiveBatch, EMPTY_BATCH_COMPONENT, isBatchableTool } from "./batch.js";
11
11
  import { editTool } from "./edit.js";
12
12
  import { renderFallbackCall, renderFallbackResult } from "./fallback.js";
13
13
  import { findTool } from "./find.js";
@@ -15,7 +15,16 @@ import { grepTool } from "./grep.js";
15
15
  import { lsTool } from "./ls.js";
16
16
  import { getQuickEditToolConfig, quickEditTool } from "./quick-edit.js";
17
17
  import { readTool } from "./read.js";
18
+ import { getStateElapsedMs, getToolsRenderConfig } from "./session-config.js";
18
19
  import type { BoxedToolContext, BoxedToolDefinition } from "./shared.js";
20
+ import {
21
+ emptyTurnResult,
22
+ getTurnEntry,
23
+ noteTurnMemberElapsed,
24
+ noteTurnMemberRender,
25
+ renderTurnSummaryCall,
26
+ type TurnState,
27
+ } from "./turn-summary.js";
19
28
  import { writeTool } from "./write.js";
20
29
 
21
30
  function quickEditToolFor(toolName: string): BoxedToolDefinition {
@@ -41,6 +50,18 @@ export function hasBoxedRenderer(toolName: unknown): boolean {
41
50
  return typeof toolName === "string" && Object.hasOwn(REGISTRY, toolName);
42
51
  }
43
52
 
53
+ /**
54
+ * Turn-summary gate (ADR 0007): the member belongs to an ended turn, Pi's
55
+ * global tool-output state is collapsed, the surface is enabled, and the block
56
+ * itself is not an error (errors always stay visible).
57
+ */
58
+ function collapsedTurnFor(toolCallId: string, expanded: boolean): TurnState | undefined {
59
+ if (expanded || !getToolsRenderConfig().collapseAfterTurn) return undefined;
60
+ const entry = getTurnEntry(toolCallId);
61
+ if (!entry?.turn.ended || entry.member.isError) return undefined;
62
+ return entry.turn;
63
+ }
64
+
44
65
  export function renderBoxedToolCall(
45
66
  toolName: unknown,
46
67
  args: Record<string, unknown>,
@@ -50,6 +71,16 @@ export function renderBoxedToolCall(
50
71
  // Any non-batchable tool call is a batch boundary: the next quiet call starts
51
72
  // a fresh batch instead of joining the previous one.
52
73
  if (!isBatchableTool(toolName)) closeActiveBatch();
74
+ const turn = collapsedTurnFor(context.toolCallId, context.expanded);
75
+ if (turn) {
76
+ if (turn.leaderId === context.toolCallId) return renderTurnSummaryCall(theme, turn);
77
+ // Same singleton the batch machinery uses: the decoration's hideBatchMember
78
+ // (identity-compared) removes the instance so members consume zero lines.
79
+ return EMPTY_BATCH_COMPONENT;
80
+ }
81
+ // Capture the component invalidate so the turn_end path can force this block
82
+ // to re-run the renderer selectors (pi only re-invokes them from updateDisplay).
83
+ noteTurnMemberRender(context.toolCallId, context.invalidate);
53
84
  const tool = typeof toolName === "string" ? REGISTRY[toolName] : undefined;
54
85
  if (tool) return tool.call(args, theme, context);
55
86
  return renderFallbackCall(toolName, args, theme, context);
@@ -62,6 +93,15 @@ export function renderBoxedToolResult(
62
93
  theme: BoxTheme,
63
94
  context: BoxedToolContext,
64
95
  ): Component {
96
+ const turn = collapsedTurnFor(context.toolCallId, options.expanded);
97
+ if (turn) {
98
+ // Freeze the member's wall-clock elapsed into the registry once the turn
99
+ // collapsed (the value is already frozen by the renderer context state).
100
+ if (!options.isPartial) noteTurnMemberElapsed(context.toolCallId, getStateElapsedMs(context.state));
101
+ if (turn.leaderId === context.toolCallId) return emptyTurnResult();
102
+ return EMPTY_BATCH_COMPONENT;
103
+ }
104
+ noteTurnMemberRender(context.toolCallId, context.invalidate);
65
105
  const tool = typeof toolName === "string" ? REGISTRY[toolName] : undefined;
66
106
  if (tool) return tool.result(result, options, theme, context);
67
107
  return renderFallbackResult(toolName, result, options, theme, context);
@@ -12,6 +12,8 @@ export interface ToolsRenderConfig {
12
12
  batchOpenGlyph: string;
13
13
  /** Nerd Font mode is active: file-type icons render in output trees. */
14
14
  nerdFonts: boolean;
15
+ /** Collapse a completed turn's tool blocks into one summary line (ADR 0007). */
16
+ collapseAfterTurn: boolean;
15
17
  }
16
18
 
17
19
  let sessionToolsConfig: ToolsRenderConfig = {
@@ -21,6 +23,7 @@ let sessionToolsConfig: ToolsRenderConfig = {
21
23
  showElapsed: true,
22
24
  batchOpenGlyph: "●",
23
25
  nerdFonts: false,
26
+ collapseAfterTurn: true,
24
27
  };
25
28
 
26
29
  export function setToolsRenderConfig(config: Partial<ToolsRenderConfig>): void {