@quandev104/pi-style 0.1.1 → 0.1.3

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 (39) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -2
  3. package/dist/extensions/pi-style.js +5881 -4929
  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/theme.ts +3 -0
  11. package/extension-src/pi-style/features/messages/boxed-block.ts +16 -20
  12. package/extension-src/pi-style/features/messages/index.ts +97 -40
  13. package/extension-src/pi-style/features/messages/special-blocks.ts +3 -3
  14. package/extension-src/pi-style/features/startup/index.ts +4 -4
  15. package/extension-src/pi-style/features/tools/boxed/bash.ts +395 -19
  16. package/extension-src/pi-style/features/tools/boxed/batch.ts +459 -0
  17. package/extension-src/pi-style/features/tools/boxed/edit.ts +39 -23
  18. package/extension-src/pi-style/features/tools/boxed/fallback.ts +10 -8
  19. package/extension-src/pi-style/features/tools/boxed/find.ts +48 -48
  20. package/extension-src/pi-style/features/tools/boxed/grep.ts +161 -89
  21. package/extension-src/pi-style/features/tools/boxed/index.ts +4 -0
  22. package/extension-src/pi-style/features/tools/boxed/ls.ts +39 -47
  23. package/extension-src/pi-style/features/tools/boxed/output-tree.ts +368 -0
  24. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +45 -24
  25. package/extension-src/pi-style/features/tools/boxed/read.ts +32 -189
  26. package/extension-src/pi-style/features/tools/boxed/session-config.ts +6 -0
  27. package/extension-src/pi-style/features/tools/boxed/write.ts +91 -49
  28. package/extension-src/pi-style/features/tools/index.ts +14 -0
  29. package/extension-src/pi-style/pi/compatibility-coordinator.ts +4 -26
  30. package/extension-src/pi-style/pi/compatibility-probe.ts +3 -33
  31. package/extension-src/pi-style/pi/compatibility-registry.ts +0 -1
  32. package/extension-src/pi-style/pi/config-session.ts +0 -2
  33. package/extension-src/pi-style/pi/index.ts +35 -1
  34. package/extension-src/pi-style/pi/session-coordinator.ts +48 -4
  35. package/extension-src/pi-style/shared/ansi.ts +3 -0
  36. package/extension-src/pi-style/shared/box.ts +210 -69
  37. package/extension-src/pi-style/shared/split-diff.ts +395 -86
  38. package/extension-src/pi-style/shared/theme-extras.ts +0 -2
  39. package/package.json +1 -1
@@ -1,204 +1,47 @@
1
1
  // Boxed read tool renderer
2
2
  // (renderCall/renderResult only; no tool re-registration).
3
+ //
4
+ // Read calls render as a boxless tree panel — a lone read is a batch of one,
5
+ // consecutive reads group into one panel (see batch.ts). There is no boxed
6
+ // single-call special case.
3
7
 
4
- import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
5
8
  import { stripAnsi } from "../../../shared/ansi.js";
6
- import type { BoxTheme } from "../../../shared/box.js";
9
+ import { getTextOutput } from "../../../shared/box.js";
7
10
  import {
8
- boxedToolWidthKey,
9
- countLines,
10
- extractTrailingNotice,
11
- getTextOutput,
12
- renderBoxedToolResult,
13
- stripTrailingNotice,
14
- } from "../../../shared/box.js";
15
- import { safeTruncateToWidth } from "../../../shared/render-budget.js";
16
- import { getToolsRenderConfig } from "./session-config.js";
17
- import {
18
- type BoxedToolDefinition,
19
- clearFooterState,
20
- compactCall,
21
- compactFooterWithState,
22
- noteExecutionStart,
23
- pathRangeDetail,
24
- resultFooterLines,
25
- truncationOutputLines,
26
- } from "./shared.js";
27
-
28
- const MAX_HIGHLIGHT_OUTPUT_CHARS = 12000;
29
- const MAX_HIGHLIGHT_OUTPUT_LINES = 300;
30
-
31
- type NumberedReadLine = {
32
- lineNumber: string;
33
- content: string;
34
- };
35
-
36
- type ParsedReadOutput = {
37
- fileHash: string | undefined;
38
- numberedLines?: NumberedReadLine[];
39
- body: string;
40
- };
41
-
42
- function parseReadOutput(text: string): ParsedReadOutput {
43
- const fileHashMatch = text.match(/^fileHash: ([^\n]+)\n\n/);
44
- const body = fileHashMatch ? text.slice(fileHashMatch[0].length) : text;
45
- const rawLines = body ? body.split("\n") : [];
46
- const numberedLines = rawLines.map((line) => line.match(/^\s*(\d+)\| ?(.*)$/));
47
-
48
- if (numberedLines.length > 0 && numberedLines.every(Boolean)) {
49
- return {
50
- fileHash: fileHashMatch?.[1],
51
- body: numberedLines.map((match) => match?.[2] ?? "").join("\n"),
52
- numberedLines: numberedLines.map((match) => ({
53
- lineNumber: match?.[1] ?? "",
54
- content: match?.[2] ?? "",
55
- })),
56
- };
57
- }
58
-
59
- return { fileHash: fileHashMatch?.[1], body };
60
- }
61
-
62
- function renderReadBody(
63
- theme: BoxTheme,
64
- options: { expanded: boolean },
65
- parsed: ParsedReadOutput,
66
- output: string,
67
- truncationNotice: string | null,
68
- ): { render(width: number): string[]; invalidate(): void } {
69
- let cacheKey = "";
70
- let cacheLines: string[] | null = null;
71
- return {
72
- invalidate() {
73
- cacheKey = "";
74
- cacheLines = null;
75
- },
76
- render(width: number): string[] {
77
- const renderWidth = Math.max(1, width);
78
- const cfg = getToolsRenderConfig();
79
- const maxLines = cfg.maxExpandedLines;
80
- const expanded = Boolean(options.expanded);
81
- const cacheId = `${renderWidth}|${expanded ? 1 : 0}|${maxLines}|${cfg.dimOutput ? 1 : 0}`;
82
- if (cacheLines && cacheKey === cacheId) return cacheLines;
83
-
84
- const linesRead = parsed.numberedLines?.length ?? countLines(parsed.body);
85
- const summary = theme.fg("dim", `↳ Read ${linesRead} ${linesRead === 1 ? "line" : "lines"}.`);
86
- const footer: string[] = [];
87
- if (truncationNotice) footer.push(theme.fg("warning", truncationNotice));
88
- footer.push("", summary);
89
- const budget = maxLines > 0 ? maxLines - footer.length : 0;
90
- const renderPlain = (text: string): string[] => {
91
- const out: string[] = [];
92
- for (const line of text.split("\n")) {
93
- out.push(safeTruncateToWidth(theme.fg("toolOutput", line), renderWidth, "…"));
94
- }
95
- return out;
96
- };
97
- const lineCount = parsed.numberedLines?.length ?? countLines(parsed.body);
98
- const shouldHighlight =
99
- expanded &&
100
- Boolean(getLanguageFromPath(output)) &&
101
- parsed.body.length <= MAX_HIGHLIGHT_OUTPUT_CHARS &&
102
- lineCount <= MAX_HIGHLIGHT_OUTPUT_LINES;
103
-
104
- const renderBody = (): string[] => {
105
- if (!parsed.numberedLines)
106
- return renderPlain(parsed.fileHash ? `fileHash: ${parsed.fileHash}\n\n${parsed.body}` : parsed.body);
107
-
108
- let bodyLines = parsed.body.split("\n").map((line) => theme.fg("toolOutput", line));
109
- const lang = getLanguageFromPath(output);
110
- if (shouldHighlight && lang) {
111
- try {
112
- bodyLines = highlightCode(parsed.body, lang);
113
- } catch {
114
- bodyLines = parsed.body.split("\n").map((line) => theme.fg("toolOutput", line));
115
- }
116
- }
117
-
118
- const out: string[] = [];
119
- if (parsed.fileHash) out.push(theme.fg("muted", `fileHash: ${parsed.fileHash}`), "");
120
-
121
- const numberWidth = Math.max(...parsed.numberedLines.map((line) => line.lineNumber.length));
122
- const gutterWidth = numberWidth + 3;
123
- const contentWidth = Math.max(1, renderWidth - gutterWidth);
124
- for (let i = 0; i < parsed.numberedLines.length; i++) {
125
- const numberedLine = parsed.numberedLines[i] ?? { lineNumber: "", content: "" };
126
- const bodyLine = safeTruncateToWidth(bodyLines[i] ?? "", contentWidth, "…");
127
- const gutter = theme.fg("dim", `${numberedLine.lineNumber.padStart(numberWidth)} │ `);
128
- out.push(`${gutter}${bodyLine}`);
129
- }
130
- return out;
131
- };
132
-
133
- const highlighted = renderBody();
134
- if (maxLines > 0 && highlighted.length > budget) {
135
- const truncated = highlighted.slice(0, budget);
136
- const remaining = highlighted.length - budget;
137
- truncated.push(theme.fg("dim", `… ${remaining} more lines`));
138
- truncated.push(...footer);
139
- cacheKey = cacheId;
140
- cacheLines = truncated;
141
- return cacheLines;
142
- }
143
- highlighted.push(...footer);
144
- cacheKey = cacheId;
145
- cacheLines = highlighted;
146
- return cacheLines;
147
- },
148
- };
149
- }
11
+ type BatchToolMeta,
12
+ EMPTY_BATCH_COMPONENT,
13
+ emptyBatchResult,
14
+ registerBatchCall,
15
+ registerBatchResult,
16
+ renderBatchAwareCall,
17
+ } from "./batch.js";
18
+ import { type BoxedToolDefinition, noteExecutionStart, pathRangeDetail } from "./shared.js";
19
+
20
+ const READ_META: BatchToolMeta = Object.freeze({
21
+ toolName: "read",
22
+ label: "Read",
23
+ });
150
24
 
151
25
  export const readTool: BoxedToolDefinition = {
152
26
  call(args, theme, context) {
153
27
  noteExecutionStart(context);
154
28
  const rawPath = String(args?.path ?? args?.file_path ?? "");
155
29
  const detail = pathRangeDetail(rawPath, args?.offset, args?.limit, context);
156
- return compactCall(theme, "Read", `${theme.fg("dim", "Path: ")}${detail}`, {
157
- detailKey: detail,
158
- context,
159
- });
30
+ const { isLeader, batch } = registerBatchCall(READ_META, detail, context);
31
+ if (!isLeader) return EMPTY_BATCH_COMPONENT;
32
+ return renderBatchAwareCall(theme, batch);
160
33
  },
161
- result(result, options, theme, context) {
162
- clearFooterState(context);
34
+ result(result, options, _theme, context) {
163
35
  const output = stripAnsi(getTextOutput(result)).trimEnd();
164
- const rawPath = String(context?.args?.path ?? context?.args?.file_path ?? "");
165
- const detail = pathRangeDetail(rawPath, context?.args?.offset, context?.args?.limit, context);
166
- const widthKey = boxedToolWidthKey("Read", detail);
167
-
168
- if (context.isError) {
169
- return renderBoxedToolResult(theme, () => [theme.fg("error", output || "Error")], {
170
- widthKey,
171
- footerLines: resultFooterLines(theme, result, context),
172
- isError: true,
173
- });
174
- }
175
-
176
- const imageCount = Array.isArray(result.content)
177
- ? result.content.filter((contentBlock) => {
178
- if (!contentBlock || typeof contentBlock !== "object") return false;
179
- return (contentBlock as { type?: unknown }).type === "image";
180
- }).length
181
- : 0;
182
- if (imageCount > 0) {
183
- if (!options.expanded) return compactFooterWithState(theme, result, context);
184
- const summary = `↳ Read ${imageCount} ${imageCount === 1 ? "image" : "images"}.`;
185
- return renderBoxedToolResult(theme, () => [theme.fg("dim", summary)], {
186
- widthKey,
187
- footerLines: resultFooterLines(theme, result, context),
188
- });
189
- }
190
-
191
- const stripped = stripTrailingNotice(output);
192
- const parsed = parseReadOutput(stripped);
193
- const truncationNotice = extractTrailingNotice(output);
194
- const _linesRead = truncationOutputLines(result) ?? parsed.numberedLines?.length ?? countLines(parsed.body);
195
-
196
- if (!options.expanded) return compactFooterWithState(theme, result, context);
197
-
198
- const body = renderReadBody(theme, options, parsed, output, truncationNotice);
199
- return renderBoxedToolResult(theme, body, {
200
- widthKey,
201
- footerLines: resultFooterLines(theme, result, context),
202
- });
36
+ registerBatchResult(
37
+ READ_META,
38
+ {
39
+ isPartial: Boolean(options.isPartial),
40
+ isError: Boolean(context.isError),
41
+ errorText: context.isError ? output || undefined : undefined,
42
+ },
43
+ context,
44
+ );
45
+ return emptyBatchResult();
203
46
  },
204
47
  };
@@ -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 {
@@ -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,39 +31,86 @@ 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
+ type NumberedLine = { number: string; content: string };
27
38
 
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
- }
39
+ /**
40
+ * Numbered preview lines for the written content, `cat -n` style: every split
41
+ * line keeps its number (including a trailing empty line produced by a final
42
+ * newline), right-aligned to the widest line number.
43
+ */
44
+ function numberedPreviewLines(content: string): NumberedLine[] {
45
+ const normalized = replaceTabs(String(content ?? "")).replace(/\r/g, "");
46
+ if (!normalized) return [];
47
+ const lines = normalized.split("\n");
48
+ const gutterWidth = Math.max(1, String(lines.length).length);
49
+ return lines.map((line, index) => ({
50
+ number: String(index + 1).padStart(gutterWidth),
51
+ content: line,
52
+ }));
53
+ }
54
+
55
+ /** One boxed preview row: dim gutter + toolOutput content. */
56
+ function formatNumberedLine(theme: BoxTheme, line: NumberedLine): string {
57
+ return `${theme.fg("borderMuted", `${line.number} `)}${theme.fg("toolOutput", line.content)}`;
58
+ }
35
59
 
36
- return undefined;
60
+ /** Compact write box: path header, numbered content preview, metrics footer. */
61
+ function renderWritePreviewBox(
62
+ theme: BoxTheme,
63
+ detailLine: string,
64
+ content: string,
65
+ options: {
66
+ state?: Record<string, unknown>;
67
+ isError: boolean;
68
+ isPending: boolean;
69
+ expanded: boolean;
70
+ },
71
+ ): Component {
72
+ const preview = numberedPreviewLines(content);
73
+ const config = getToolsRenderConfig();
74
+ const budget = options.expanded ? config.maxExpandedLines : config.maxCollapsedLines;
75
+ const truncated = preview.length > budget;
76
+
77
+ return renderCompactBoxedToolCall(theme, "Write", detailLine, {
78
+ ...(options.state ? { state: options.state } : {}),
79
+ isError: options.isError,
80
+ isPending: options.isPending,
81
+ bodyLines: () => {
82
+ if (preview.length === 0) return [];
83
+ const shown = preview.slice(0, budget).map((line) => formatNumberedLine(theme, line));
84
+ if (!truncated) return shown;
85
+ const omitted = preview.length - budget;
86
+ const note = options.expanded ? `… ${omitted} more lines omitted by render budget` : `… ${omitted} more lines`;
87
+ return [...shown, theme.fg("muted", note)];
88
+ },
89
+ ...(options.expanded || options.isPending || !truncated ? {} : { bottomRightLabel: WRITE_EXPAND_HINT }),
90
+ });
37
91
  }
38
92
 
39
93
  export const writeTool: BoxedToolDefinition = {
40
94
  call(args, theme, context) {
41
95
  noteExecutionStart(context);
42
96
  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,
97
+ const detailLine = `${theme.fg("dim", "Path: ")}${detail}`;
98
+ // On error keep the plain open box: the result renderer continues it with
99
+ // the boxed error body, so call and result never duplicate a box.
100
+ if (context.isError) {
101
+ return compactCall(theme, "Write", detailLine, {
102
+ detailKey: detail,
103
+ context,
104
+ });
105
+ }
106
+ return renderWritePreviewBox(theme, detailLine, String(args?.content ?? ""), {
107
+ state: context.state,
108
+ isError: Boolean(context.isError),
109
+ isPending: Boolean(context.isPartial),
110
+ expanded: Boolean(context.expanded),
46
111
  });
47
112
  },
48
- result(result, options, theme, context) {
113
+ result(result, _options, theme, context) {
49
114
  clearFooterState(context);
50
115
  const output = getTextOutput(result);
51
116
  const detail = displayPath(String(context?.args?.path ?? context?.args?.file_path ?? ""), context);
@@ -59,31 +124,8 @@ export const writeTool: BoxedToolDefinition = {
59
124
  });
60
125
  }
61
126
 
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
- }
81
-
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
- });
127
+ // Success (compact and expanded): the preview box closes with the metrics
128
+ // footer stored into the shared renderer state; the result adds nothing.
129
+ return compactFooterWithState(theme, result, context);
88
130
  },
89
131
  };
@@ -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,