@quandev104/pi-style 0.1.2 → 0.1.4

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -2
  3. package/dist/extensions/pi-style.js +5919 -4966
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/command-service.ts +2 -2
  6. package/extension-src/pi-style/app/index.ts +0 -1
  7. package/extension-src/pi-style/domain/config-authorization.ts +1 -2
  8. package/extension-src/pi-style/domain/config-normalization.ts +3 -3
  9. package/extension-src/pi-style/domain/config-types.ts +2 -2
  10. package/extension-src/pi-style/domain/status.ts +1 -1
  11. package/extension-src/pi-style/domain/theme.ts +3 -0
  12. package/extension-src/pi-style/features/messages/index.ts +2 -8
  13. package/extension-src/pi-style/features/tools/boxed/bash.ts +663 -45
  14. package/extension-src/pi-style/features/tools/boxed/batch.ts +459 -0
  15. package/extension-src/pi-style/features/tools/boxed/edit.ts +28 -2
  16. package/extension-src/pi-style/features/tools/boxed/fallback.ts +47 -4
  17. package/extension-src/pi-style/features/tools/boxed/find.ts +48 -48
  18. package/extension-src/pi-style/features/tools/boxed/grep.ts +161 -89
  19. package/extension-src/pi-style/features/tools/boxed/index.ts +4 -0
  20. package/extension-src/pi-style/features/tools/boxed/ls.ts +39 -47
  21. package/extension-src/pi-style/features/tools/boxed/output-tree.ts +368 -0
  22. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +25 -4
  23. package/extension-src/pi-style/features/tools/boxed/read.ts +32 -189
  24. package/extension-src/pi-style/features/tools/boxed/session-config.ts +70 -4
  25. package/extension-src/pi-style/features/tools/boxed/shared.ts +41 -1
  26. package/extension-src/pi-style/features/tools/boxed/write.ts +105 -47
  27. package/extension-src/pi-style/features/tools/index.ts +14 -0
  28. package/extension-src/pi-style/pi/compatibility-coordinator.ts +4 -26
  29. package/extension-src/pi-style/pi/compatibility-probe.ts +3 -33
  30. package/extension-src/pi-style/pi/compatibility-registry.ts +0 -1
  31. package/extension-src/pi-style/pi/config-session.ts +0 -2
  32. package/extension-src/pi-style/pi/index.ts +35 -1
  33. package/extension-src/pi-style/pi/session-coordinator.ts +47 -3
  34. package/extension-src/pi-style/shared/box.ts +117 -26
  35. package/extension-src/pi-style/shared/theme-extras.ts +0 -2
  36. package/package.json +1 -1
@@ -8,6 +8,10 @@ export interface ToolsRenderConfig {
8
8
  maxExpandedLines: number;
9
9
  dimOutput: boolean;
10
10
  showElapsed: boolean;
11
+ /** Open-tree glyph for the done batch header (nerd `\u{F111}` / unicode `●`). */
12
+ batchOpenGlyph: string;
13
+ /** Nerd Font mode is active: file-type icons render in output trees. */
14
+ nerdFonts: boolean;
11
15
  }
12
16
 
13
17
  let sessionToolsConfig: ToolsRenderConfig = {
@@ -15,6 +19,8 @@ let sessionToolsConfig: ToolsRenderConfig = {
15
19
  maxExpandedLines: 50,
16
20
  dimOutput: false,
17
21
  showElapsed: true,
22
+ batchOpenGlyph: "●",
23
+ nerdFonts: false,
18
24
  };
19
25
 
20
26
  export function setToolsRenderConfig(config: Partial<ToolsRenderConfig>): void {
@@ -27,19 +33,79 @@ export function getToolsRenderConfig(): ToolsRenderConfig {
27
33
 
28
34
  // Wall-clock elapsed tracking through the renderer context state (no tool
29
35
  // re-registration, so result.details has no execution timing).
36
+ //
37
+ // Elapsed is computed live from the recorded start on every read, so a running
38
+ // tool keeps growing its displayed time. The value only freezes once the
39
+ // execution end is recorded (terminal result), which keeps the completed footer
40
+ // stable across later re-renders (expand toggles, terminal resizes).
30
41
 
31
42
  const STARTED_AT_KEY = "__piStyleStartedAt";
32
- const ELAPSED_MS_KEY = "__piStyleElapsedMs";
43
+ const ENDED_AT_KEY = "__piStyleEndedAt";
44
+ const RESULT_SEEN_KEY = "__piStyleResultSeen";
45
+ const TICKER_KEY = "__piStyleElapsedTicker";
33
46
 
34
47
  export function recordExecutionStarted(state: Record<string, unknown> | undefined, executionStarted: boolean): void {
35
48
  if (!executionStarted || !state || typeof state !== "object") return;
36
49
  if (typeof state[STARTED_AT_KEY] !== "number") state[STARTED_AT_KEY] = performance.now();
37
50
  }
38
51
 
52
+ /** Freeze the elapsed at the first terminal render (idempotent). */
53
+ export function recordExecutionEnded(state: Record<string, unknown> | undefined): void {
54
+ if (!state || typeof state !== "object") return;
55
+ if (typeof state[ENDED_AT_KEY] !== "number") state[ENDED_AT_KEY] = performance.now();
56
+ }
57
+
39
58
  export function getStateElapsedMs(state: Record<string, unknown> | undefined): number | undefined {
40
59
  if (!state || typeof state !== "object") return undefined;
41
- if (typeof state[ELAPSED_MS_KEY] !== "number" && typeof state[STARTED_AT_KEY] === "number") {
42
- state[ELAPSED_MS_KEY] = performance.now() - state[STARTED_AT_KEY];
60
+ const started = state[STARTED_AT_KEY];
61
+ if (typeof started !== "number") return undefined;
62
+ const ended = state[ENDED_AT_KEY];
63
+ if (typeof ended === "number") return Math.max(0, ended - started);
64
+ return Math.max(0, performance.now() - started);
65
+ }
66
+
67
+ /** Whether a result renderer already produced a continuation for this call. */
68
+ export function isResultSeen(state: Record<string, unknown> | undefined): boolean {
69
+ return Boolean(state && typeof state === "object" && state[RESULT_SEEN_KEY] === true);
70
+ }
71
+
72
+ /** Record that a result renderer ran for this call (streaming or final). */
73
+ export function markResultSeen(state: Record<string, unknown> | undefined): void {
74
+ if (!state || typeof state !== "object") return;
75
+ state[RESULT_SEEN_KEY] = true;
76
+ }
77
+
78
+ type TickerHandle = ReturnType<typeof setInterval>;
79
+
80
+ /** States that currently own a 1s elapsed-render interval, for session cleanup. */
81
+ const tickerStates = new Set<Record<string, unknown>>();
82
+
83
+ /**
84
+ * While a tool is running, re-render once per second so live elapsed labels
85
+ * tick without any output events. Idempotent per state.
86
+ */
87
+ export function startElapsedTicker(state: Record<string, unknown> | undefined, invalidate: () => void): void {
88
+ if (!state || typeof state !== "object") return;
89
+ if (state[TICKER_KEY] !== undefined) return;
90
+ state[TICKER_KEY] = setInterval(() => invalidate(), 1000) as unknown as TickerHandle;
91
+ tickerStates.add(state);
92
+ }
93
+
94
+ /** Stop a running tool's elapsed ticker (terminal result, error, session end). */
95
+ export function stopElapsedTicker(state: Record<string, unknown> | undefined): void {
96
+ if (!state || typeof state !== "object") return;
97
+ const handle = state[TICKER_KEY] as TickerHandle | undefined;
98
+ if (handle !== undefined) clearInterval(handle);
99
+ delete state[TICKER_KEY];
100
+ tickerStates.delete(state);
101
+ }
102
+
103
+ /** Stop every elapsed ticker (session start/shutdown). */
104
+ export function stopAllElapsedTickers(): void {
105
+ for (const state of tickerStates) {
106
+ const handle = state[TICKER_KEY] as TickerHandle | undefined;
107
+ if (handle !== undefined) clearInterval(handle);
108
+ delete state[TICKER_KEY];
43
109
  }
44
- return typeof state[ELAPSED_MS_KEY] === "number" ? state[ELAPSED_MS_KEY] : undefined;
110
+ tickerStates.clear();
45
111
  }
@@ -13,12 +13,21 @@ import {
13
13
  resolveRelativePath,
14
14
  shortenPath,
15
15
  } from "../../../shared/box.js";
16
- import { getStateElapsedMs, recordExecutionStarted } from "./session-config.js";
16
+ import {
17
+ getStateElapsedMs,
18
+ isResultSeen,
19
+ markResultSeen,
20
+ recordExecutionEnded,
21
+ recordExecutionStarted,
22
+ startElapsedTicker,
23
+ stopElapsedTicker,
24
+ } from "./session-config.js";
17
25
 
18
26
  /** Renderer context delivered by Pi's ToolExecutionComponent (getRenderContext). */
19
27
  export interface BoxedToolContext {
20
28
  readonly args: Record<string, unknown>;
21
29
  readonly toolCallId: string;
30
+ readonly invalidate: () => void;
22
31
  readonly state: Record<string, unknown>;
23
32
  readonly cwd: string;
24
33
  readonly executionStarted: boolean;
@@ -88,6 +97,7 @@ export function compactCall(
88
97
  isError: Boolean(options.context.isError),
89
98
  isPartial: Boolean(options.context.isPartial),
90
99
  isPending: pendingFlag(options.context),
100
+ running: Boolean(options.context.executionStarted),
91
101
  });
92
102
  }
93
103
 
@@ -96,6 +106,36 @@ export function noteExecutionStart(context: BoxedToolContext): void {
96
106
  recordExecutionStarted(context.state, context.executionStarted);
97
107
  }
98
108
 
109
+ /**
110
+ * Keep running/ended execution state in sync from a call renderer pass. While
111
+ * the tool runs, a 1s re-render ticker keeps live elapsed labels current; once
112
+ * the call renders in its terminal form the elapsed freezes.
113
+ */
114
+ export function noteBoxedCallState(context: BoxedToolContext): void {
115
+ if (!context.executionStarted) return;
116
+ if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
117
+ else {
118
+ recordExecutionEnded(context.state);
119
+ stopElapsedTicker(context.state);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Record a result renderer pass and keep the ticker/ended state in sync.
125
+ * Returns whether this is the first result pass for the call, so renderers can
126
+ * render nothing while the pending/running call card stands alone.
127
+ */
128
+ export function noteBoxedResultPhase(context: BoxedToolContext, isPartial: boolean): boolean {
129
+ const firstResultPass = !isResultSeen(context.state);
130
+ markResultSeen(context.state);
131
+ if (isPartial) startElapsedTicker(context.state, context.invalidate);
132
+ else {
133
+ recordExecutionEnded(context.state);
134
+ stopElapsedTicker(context.state);
135
+ }
136
+ return firstResultPass;
137
+ }
138
+
99
139
  export function stateElapsedMs(context: BoxedToolContext): number | undefined {
100
140
  return getStateElapsedMs(context.state);
101
141
  }
@@ -1,8 +1,26 @@
1
1
  // Boxed write tool renderer
2
2
  // (renderCall/renderResult only).
3
+ //
4
+ // The write call renders a compact preview box: the file path in the top
5
+ // border, the written content as numbered lines in the body (cat -n style),
6
+ // and the metrics footer in the bottom border. The footer lives in the shared
7
+ // renderer state — the result renderer stores it (elapsed + words), the call
8
+ // component reads it at paint time and closes the box. The preview is capped
9
+ // at the collapsed line budget with a `Ctrl+O for more` hint on the bottom
10
+ // border when truncated; expanded shows the expanded budget. Errors keep the
11
+ // plain open call box so the boxed error result never duplicates a box.
3
12
 
13
+ import type { Component } from "@earendil-works/pi-tui";
4
14
  import { stripAnsi } from "../../../shared/ansi.js";
5
- import { boxedToolWidthKey, getTextOutput, renderBoxedToolResult, stripTrailingNotice } from "../../../shared/box.js";
15
+ import {
16
+ type BoxTheme,
17
+ boxedToolWidthKey,
18
+ getTextOutput,
19
+ renderBoxedToolResult,
20
+ renderCompactBoxedToolCall,
21
+ replaceTabs,
22
+ } from "../../../shared/box.js";
23
+ import { getToolsRenderConfig } from "./session-config.js";
6
24
  import {
7
25
  type BoxedToolDefinition,
8
26
  clearFooterState,
@@ -13,36 +31,94 @@ import {
13
31
  resultFooterLines,
14
32
  } from "./shared.js";
15
33
 
16
- function parseWriteSummary(output: string): string | undefined {
17
- const normalized = stripTrailingNotice(stripAnsi(output ?? "")).trim();
18
- if (!normalized) return undefined;
34
+ /** Right-side bottom-border hint shown when the compact preview is truncated. */
35
+ const WRITE_EXPAND_HINT = "Ctrl+O for more";
19
36
 
20
- const byteMatch = normalized.match(/\bwrote\s+(\d+)\s+bytes?\b/i);
21
- if (byteMatch) {
22
- const bytes = Number(byteMatch[1]);
23
- if (Number.isFinite(bytes)) {
24
- return `↳ Wrote ${bytes} ${bytes === 1 ? "byte" : "bytes"}.`;
25
- }
26
- }
37
+ /** Partial-pass result: the compact call keeps its `◌ Running` card. */
38
+ const EMPTY_WRITE_RESULT: Component = Object.freeze({
39
+ invalidate() {},
40
+ render() {
41
+ return [];
42
+ },
43
+ });
27
44
 
28
- const lineMatch = normalized.match(/\bwrote\s+(\d+)\s+lines?\b/i);
29
- if (lineMatch) {
30
- const count = Number(lineMatch[1]);
31
- if (Number.isFinite(count)) {
32
- return `↳ Wrote ${count} ${count === 1 ? "line" : "lines"}.`;
33
- }
34
- }
45
+ type NumberedLine = { number: string; content: string };
46
+
47
+ /**
48
+ * Numbered preview lines for the written content, `cat -n` style: every split
49
+ * line keeps its number (including a trailing empty line produced by a final
50
+ * newline), right-aligned to the widest line number.
51
+ */
52
+ function numberedPreviewLines(content: string): NumberedLine[] {
53
+ const normalized = replaceTabs(String(content ?? "")).replace(/\r/g, "");
54
+ if (!normalized) return [];
55
+ const lines = normalized.split("\n");
56
+ const gutterWidth = Math.max(1, String(lines.length).length);
57
+ return lines.map((line, index) => ({
58
+ number: String(index + 1).padStart(gutterWidth),
59
+ content: line,
60
+ }));
61
+ }
35
62
 
36
- return undefined;
63
+ /** One boxed preview row: dim gutter + toolOutput content. */
64
+ function formatNumberedLine(theme: BoxTheme, line: NumberedLine): string {
65
+ return `${theme.fg("borderMuted", `${line.number} `)}${theme.fg("toolOutput", line.content)}`;
66
+ }
67
+
68
+ /** Compact write box: path header, numbered content preview, metrics footer. */
69
+ function renderWritePreviewBox(
70
+ theme: BoxTheme,
71
+ detailLine: string,
72
+ content: string,
73
+ options: {
74
+ state?: Record<string, unknown>;
75
+ isError: boolean;
76
+ isPending: boolean;
77
+ running?: boolean;
78
+ expanded: boolean;
79
+ },
80
+ ): Component {
81
+ const preview = numberedPreviewLines(content);
82
+ const config = getToolsRenderConfig();
83
+ const budget = options.expanded ? config.maxExpandedLines : config.maxCollapsedLines;
84
+ const truncated = preview.length > budget;
85
+
86
+ return renderCompactBoxedToolCall(theme, "Write", detailLine, {
87
+ ...(options.state ? { state: options.state } : {}),
88
+ isError: options.isError,
89
+ isPending: options.isPending,
90
+ running: Boolean(options.running),
91
+ bodyLines: () => {
92
+ if (preview.length === 0) return [];
93
+ const shown = preview.slice(0, budget).map((line) => formatNumberedLine(theme, line));
94
+ if (!truncated) return shown;
95
+ const omitted = preview.length - budget;
96
+ const note = options.expanded ? `… ${omitted} more lines omitted by render budget` : `… ${omitted} more lines`;
97
+ return [...shown, theme.fg("muted", note)];
98
+ },
99
+ ...(options.expanded || options.isPending || !truncated ? {} : { bottomRightLabel: WRITE_EXPAND_HINT }),
100
+ });
37
101
  }
38
102
 
39
103
  export const writeTool: BoxedToolDefinition = {
40
104
  call(args, theme, context) {
41
105
  noteExecutionStart(context);
42
106
  const detail = displayPath(String(args?.path ?? args?.file_path ?? ""), context);
43
- return compactCall(theme, "Write", `${theme.fg("dim", "Path: ")}${detail}`, {
44
- detailKey: detail,
45
- context,
107
+ const detailLine = `${theme.fg("dim", "Path: ")}${detail}`;
108
+ // On error keep the plain open box: the result renderer continues it with
109
+ // the boxed error body, so call and result never duplicate a box.
110
+ if (context.isError) {
111
+ return compactCall(theme, "Write", detailLine, {
112
+ detailKey: detail,
113
+ context,
114
+ });
115
+ }
116
+ return renderWritePreviewBox(theme, detailLine, String(args?.content ?? ""), {
117
+ state: context.state,
118
+ isError: Boolean(context.isError),
119
+ isPending: Boolean(context.isPartial),
120
+ running: Boolean(context.executionStarted),
121
+ expanded: Boolean(context.expanded),
46
122
  });
47
123
  },
48
124
  result(result, options, theme, context) {
@@ -59,31 +135,13 @@ export const writeTool: BoxedToolDefinition = {
59
135
  });
60
136
  }
61
137
 
62
- if (!options.expanded) return compactFooterWithState(theme, result, context);
63
-
64
- const content = String(context?.args?.content ?? "");
65
- const lineCount = content ? content.split("\n").length : 0;
66
- if (lineCount > 0) {
67
- const summary = `↳ Wrote ${lineCount} ${lineCount === 1 ? "line" : "lines"}.`;
68
- return renderBoxedToolResult(theme, () => [theme.fg("dim", summary)], {
69
- widthKey,
70
- footerLines: resultFooterLines(theme, result, context),
71
- });
72
- }
73
-
74
- const summary = parseWriteSummary(output);
75
- if (summary) {
76
- return renderBoxedToolResult(theme, () => [theme.fg("dim", summary)], {
77
- widthKey,
78
- footerLines: resultFooterLines(theme, result, context),
79
- });
80
- }
138
+ // While the result is still streaming, don't stamp a metrics footer into
139
+ // the shared state: the compact call keeps its `◌ Running` card and only
140
+ // closes with `elapsed · words` once the tool settles.
141
+ if (options.isPartial) return EMPTY_WRITE_RESULT;
81
142
 
82
- const normalized = stripTrailingNotice(stripAnsi(output)).trim();
83
- const fallback = normalized ? `↳ ${normalized}` : "↳ Wrote file.";
84
- return renderBoxedToolResult(theme, () => [theme.fg("dim", fallback)], {
85
- widthKey,
86
- footerLines: resultFooterLines(theme, result, context),
87
- });
143
+ // Success (compact and expanded): the preview box closes with the metrics
144
+ // footer stored into the shared renderer state; the result adds nothing.
145
+ return compactFooterWithState(theme, result, context);
88
146
  },
89
147
  };
@@ -1,6 +1,19 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { EMPTY_BATCH_COMPONENT } from "./boxed/batch.js";
2
3
  import { renderBoxedToolCall, renderBoxedToolResult } from "./boxed/index.js";
3
4
 
5
+ /**
6
+ * Batch members render zero lines. Pi's ToolExecutionComponent always adds a
7
+ * built-in Spacer child (one blank line) and only sets hideComponent when
8
+ * hasContent is false — but adding an empty renderer still marks hasContent
9
+ * true. Mark the instance hidden so members contribute zero lines (no stray
10
+ * blank margin after the batch panel). updateDisplay resets hideComponent on
11
+ * every pass; the wrapper re-applies it on each dispatch.
12
+ */
13
+ function hideBatchMember(instance: object): void {
14
+ (instance as { hideComponent?: boolean }).hideComponent = true;
15
+ }
16
+
4
17
  /**
5
18
  * Neutralize the native ToolExecutionComponent status background for boxed
6
19
  * rendering: Pi's updateDisplay sets contentBox/selfRenderContainer bgFn to
@@ -434,6 +447,7 @@ export function createToolDecorationOwner(snapshot: Partial<ToolDecorationSnapsh
434
447
  );
435
448
  })();
436
449
  neutralizeToolContainerBackground(instance);
450
+ if (component === EMPTY_BATCH_COMPONENT) hideBatchMember(instance);
437
451
  return component;
438
452
  };
439
453
  }
@@ -11,7 +11,6 @@ import {
11
11
  export interface CompatibilityCoordinator {
12
12
  captureAuthorization(
13
13
  coreFlag: boolean,
14
- userFlag: boolean,
15
14
  assistantFlag: boolean,
16
15
  specialBlocksFlag: boolean,
17
16
  toolsFlag: boolean,
@@ -33,7 +32,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
33
32
  let authorization:
34
33
  | {
35
34
  core: boolean;
36
- user: boolean;
37
35
  assistant: boolean;
38
36
  specialBlocks: boolean;
39
37
  tools: boolean;
@@ -44,15 +42,13 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
44
42
  get report() {
45
43
  return report;
46
44
  },
47
- captureAuthorization(core, user, assistant, specialBlocks, tools, ascii) {
48
- authorization = { core, user, assistant, specialBlocks, tools, ascii };
45
+ captureAuthorization(core, assistant, specialBlocks, tools, ascii) {
46
+ authorization = { core, assistant, specialBlocks, tools, ascii };
49
47
  },
50
48
  state(config) {
51
49
  const version = detectPiVersion();
52
50
  const messagesConfigured =
53
- config.enabled &&
54
- config.messages.enabled &&
55
- (config.messages.userPrefix || config.messages.assistantPrefix || config.messages.specialBlocks);
51
+ config.enabled && config.messages.enabled && (config.messages.assistantPrefix || config.messages.specialBlocks);
56
52
  const toolsConfigured = config.enabled && config.tools.enabled;
57
53
  const surface = (
58
54
  feature: "messages" | "tools",
@@ -91,12 +87,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
91
87
  nativeFallbacks: report?.unsupported.filter((item) => item.reason.includes("fallback")).length ?? 0,
92
88
  piVersion: version.version ?? report?.piVersion ?? "unknown",
93
89
  versionRange: report?.versionRange ?? ">=0.83.0 <0.84.0",
94
- userMessage: surface(
95
- "messages",
96
- config.enabled && config.messages.enabled && config.messages.userPrefix,
97
- Boolean(authorization?.user),
98
- "native-user-message",
99
- ),
100
90
  assistantMessage: surface(
101
91
  "messages",
102
92
  config.enabled && config.messages.enabled && config.messages.assistantPrefix,
@@ -116,15 +106,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
116
106
  if (cleanupPending && !report) cleanupPending = false;
117
107
  if (cleanupPending || !tui || !config.enabled || !authorization?.core || productDenied) return undefined;
118
108
  const certifiedHost = detectPiVersion().version === "0.83.0";
119
- const userEnabled =
120
- authorization.user &&
121
- isTierCAuthorized({
122
- certifiedHost,
123
- coreFlag: authorization.core,
124
- surfaceFlag: true,
125
- surface: "userMessage",
126
- config,
127
- });
128
109
  const assistantEnabled =
129
110
  authorization.assistant &&
130
111
  isTierCAuthorized({
@@ -143,7 +124,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
143
124
  surface: "specialBlocks",
144
125
  config,
145
126
  });
146
- const messagesEnabled = (userEnabled || assistantEnabled || specialBlocksEnabled) && config.messages.enabled;
127
+ const messagesEnabled = (assistantEnabled || specialBlocksEnabled) && config.messages.enabled;
147
128
  const toolsEnabled =
148
129
  authorization.tools &&
149
130
  isTierCAuthorized({ certifiedHost, coreFlag: authorization.core, surfaceFlag: true, surface: "tools", config });
@@ -155,7 +136,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
155
136
  messages: {
156
137
  ...config.messages,
157
138
  enabled: messagesEnabled,
158
- userPrefix: userEnabled,
159
139
  assistantPrefix: assistantEnabled,
160
140
  specialBlocks: messagesEnabled && config.messages.specialBlocks && specialBlocksEnabled,
161
141
  },
@@ -165,9 +145,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
165
145
  },
166
146
  },
167
147
  messageSnapshot: {
168
- userPrefix: authorization.ascii ? "[user] " : "❯ ",
169
148
  assistantPrefix: authorization.ascii ? "[assistant] " : "│ ",
170
- userEnabled,
171
149
  assistantEnabled,
172
150
  },
173
151
  toolSnapshot: {
@@ -8,7 +8,6 @@ import {
8
8
  CustomMessageComponent,
9
9
  SkillInvocationMessageComponent,
10
10
  ToolExecutionComponent,
11
- UserMessageComponent,
12
11
  } from "@earendil-works/pi-coding-agent";
13
12
  import { decorateMessageRender, type MessageDecorationSnapshot } from "../features/messages/index.js";
14
13
  import { renderSpecialMessageBlock, type SpecialBlockSubtype } from "../features/messages/special-blocks.js";
@@ -31,7 +30,6 @@ const reportStates = new WeakMap<
31
30
  // code loaded before this module could spoof the same function source. We therefore
32
31
  // fail closed on every unrecorded Pi build and never use module-load capture as trust.
33
32
  export const TRUSTED_NATIVE_FINGERPRINTS: Readonly<Record<string, string>> = Object.freeze({
34
- "native-user-message:render": "b442a17c",
35
33
  "native-assistant-message:render": "2a39243f",
36
34
  "native-compaction-message:updateDisplay": "f8c44e78",
37
35
  "native-branch-message:updateDisplay": "415d57b7",
@@ -43,19 +41,6 @@ export const TRUSTED_NATIVE_FINGERPRINTS: Readonly<Record<string, string>> = Obj
43
41
 
44
42
  export const CERTIFICATION_TABLE = Object.freeze({
45
43
  "0.83.0": Object.freeze({
46
- "native-user-message:render": Object.freeze({
47
- feature: "messages",
48
- subtype: "native-user-message",
49
- target: UserMessageComponent.prototype,
50
- method: "render",
51
- writable: true,
52
- configurable: true,
53
- name: "render",
54
- arity: 1,
55
- fingerprint: TRUSTED_NATIVE_FINGERPRINTS["native-user-message:render"],
56
- adapterId: "message-prefix-osc133-v1",
57
- status: "certified" as const,
58
- }),
59
44
  "native-assistant-message:render": Object.freeze({
60
45
  feature: "messages",
61
46
  subtype: "native-assistant-message",
@@ -232,14 +217,6 @@ function trustedNativeIdentity(spec: TargetSpec, piVersion: string | undefined):
232
217
  }
233
218
 
234
219
  export const targetSpecs: readonly TargetSpec[] = [
235
- {
236
- feature: "messages",
237
- subtype: "native-user-message",
238
- target: UserMessageComponent.prototype,
239
- method: "render",
240
- adapterId: "message-prefix-osc133-v1",
241
- status: "certified",
242
- },
243
220
  {
244
221
  feature: "messages",
245
222
  subtype: "native-assistant-message",
@@ -404,7 +381,7 @@ function shape(target: object, method: string): boolean {
404
381
  export interface CompatibilityProbeOptions {
405
382
  markers?: Set<string>;
406
383
  config?: Readonly<{
407
- messages: { enabled: boolean; userPrefix: boolean; assistantPrefix: boolean; specialBlocks: boolean };
384
+ messages: { enabled: boolean; assistantPrefix: boolean; specialBlocks: boolean };
408
385
  tools: { enabled: boolean; style: string; maxCollapsedLines: number; maxExpandedLines: number; dimOutput: boolean };
409
386
  preset: string;
410
387
  }>;
@@ -450,7 +427,6 @@ function surfaceDisabled(spec: TargetSpec, config: CompatibilityProbeOptions["co
450
427
  if (!config) return false;
451
428
  if (spec.feature === "tools") return !config.tools.enabled;
452
429
  if (!config.messages.enabled) return true;
453
- if (spec.subtype === "native-user-message") return !config.messages.userPrefix;
454
430
  if (spec.subtype === "native-assistant-message") return !config.messages.assistantPrefix;
455
431
  if (isSpecialBlock(spec)) return !config.messages.specialBlocks;
456
432
  return true;
@@ -496,14 +472,8 @@ function probeSpec(options: {
496
472
  args,
497
473
  ) ?? Reflect.apply(original as (...values: unknown[]) => unknown, target, args)
498
474
  );
499
- if (spec.subtype === "native-user-message" || spec.subtype === "native-assistant-message")
500
- return decorateMessageRender(
501
- spec.subtype as "native-user-message" | "native-assistant-message",
502
- original,
503
- target,
504
- args,
505
- messageSnapshot,
506
- );
475
+ if (spec.subtype === "native-assistant-message")
476
+ return decorateMessageRender(original, target, args, messageSnapshot);
507
477
  return renderSpecialMessageBlock(spec.subtype as SpecialBlockSubtype, original, target, args);
508
478
  },
509
479
  });
@@ -1,5 +1,4 @@
1
1
  export type CompatibilitySubtype =
2
- | "native-user-message"
3
2
  | "native-assistant-message"
4
3
  | "native-compaction-message"
5
4
  | "native-branch-message"
@@ -8,7 +8,6 @@ export interface SessionFlagReader {
8
8
  }
9
9
  export interface SessionAuthorization {
10
10
  core: boolean;
11
- user: boolean;
12
11
  assistant: boolean;
13
12
  specialBlocks: boolean;
14
13
  tools: boolean;
@@ -59,7 +58,6 @@ export function resolveProductGate(
59
58
  export function readSessionAuthorization(pi: SessionFlagReader): SessionAuthorization {
60
59
  return {
61
60
  core: pi.getFlag("pi-style-core-patches") === true,
62
- user: pi.getFlag("pi-style-message-user") === true,
63
61
  assistant: pi.getFlag("pi-style-message-assistant") === true,
64
62
  specialBlocks: pi.getFlag("pi-style-message-special-blocks") === true,
65
63
  tools: pi.getFlag("pi-style-tools") === true,
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { StatusSnapshot } from "../domain/status.js";
3
+ import { closeActiveBatch } from "../features/tools/boxed/batch.js";
3
4
  import { registerPiStyleCommand } from "./commands.js";
4
5
  import { type CompatibilityTestHooks, createPiStyleSessionCoordinator } from "./session-coordinator.js";
5
6
  import { usageFromSession } from "./session-usage.js";
@@ -10,6 +11,24 @@ function usagePatch(ctx: ExtensionContext): StatusSnapshot {
10
11
  return usage ? { usage } : {};
11
12
  }
12
13
 
14
+ /**
15
+ * Add Pi's read-only tools (grep/find/ls) to the active tool set if they are
16
+ * registered. Preserves any other active tools (e.g. extension tools).
17
+ * Only calls setActiveTools when something actually changed.
18
+ */
19
+ function activateReadOnlyTools(pi: ExtensionAPI): void {
20
+ const available = new Set(pi.getAllTools().map((tool) => tool.name));
21
+ const active = new Set(pi.getActiveTools());
22
+ let changed = false;
23
+ for (const name of ["grep", "find", "ls"] as const) {
24
+ if (available.has(name) && !active.has(name)) {
25
+ active.add(name);
26
+ changed = true;
27
+ }
28
+ }
29
+ if (changed) pi.setActiveTools([...active]);
30
+ }
31
+
13
32
  let compatibilityTestHooks: CompatibilityTestHooks = {};
14
33
  export function __setCompatibilityTestHooks(hooks: CompatibilityTestHooks): () => void {
15
34
  const previous = compatibilityTestHooks;
@@ -26,10 +45,10 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
26
45
  // product gate `compatibility.allowCorePatches: false` (or `enabled: false`) in config.
27
46
  for (const [name, description] of [
28
47
  ["pi-style-core-patches", "Enable pi-style message/tool core patches"],
29
- ["pi-style-message-user", "Enable pi-style user message prefix"],
30
48
  ["pi-style-message-assistant", "Enable pi-style assistant message prefix"],
31
49
  ["pi-style-message-special-blocks", "Enable pi-style boxed compaction/skill/branch/custom message blocks"],
32
50
  ["pi-style-tools", "Enable pi-style tool renderer decoration"],
51
+ ["pi-style-readonly-tools", "Enable grep/find/ls read-only tools in the active tool set"],
33
52
  ] as const)
34
53
  pi.registerFlag(name, { type: "boolean", description, default: true });
35
54
  // ASCII markers stay opt-in; unicode markers are the default.
@@ -37,6 +56,13 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
37
56
  const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
38
57
  registerPiStyleCommand(pi, coordinator.app);
39
58
  pi.on("session_start", async (event, ctx) => {
59
+ // Pi only activates read/bash/edit/write by default; grep/find/ls are
60
+ // registered but inactive (kept out of the model's tool list to keep the
61
+ // core small). Activate them so the TUI shows them and the model can call
62
+ // them directly, mirroring Claude Code's glob/grep/read tool set.
63
+ if (pi.getFlag("pi-style-readonly-tools") === true) {
64
+ activateReadOnlyTools(pi);
65
+ }
40
66
  await coordinator.start(event, ctx);
41
67
  });
42
68
  pi.on("agent_start", () => coordinator.app.runtime.current?.dismissStartup());
@@ -54,6 +80,14 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
54
80
  );
55
81
  pi.on("thinking_level_select", (event) => coordinator.app.update({ thinkingLevel: event.level }, "immediate"));
56
82
  pi.on("session_info_changed", (event) => coordinator.app.update({ sessionName: event.name }, "coalesced"));
83
+ pi.on("message_start", () => {
84
+ // A new message is a batch boundary: quiet-tool (read/ls/find) calls of the
85
+ // new message start a fresh batch instead of joining the previous one.
86
+ closeActiveBatch();
87
+ // grep/bash tree panels are NOT cleared here: historical panels must keep
88
+ // their state so Pi re-renders of previous messages (scroll/resume) stay
89
+ // intact. Only session boundaries reset them (session-coordinator).
90
+ });
57
91
  pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
58
92
  // Usage (tokens + cost) is aggregated from finalized session entries at
59
93
  // message/turn boundaries, mirroring Pi's native footer; per-chunk updates