pi-zentui 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.
package/README.md CHANGED
@@ -28,6 +28,7 @@ Zentui brings two popular aesthetics to Pi:
28
28
  - Bordered input box with accent-colored left rail
29
29
  - Model name and provider displayed inside the editor frame
30
30
  - Thinking level indicator when enabled
31
+ - Prompt-box-style user messages matching the ZentUI input chrome
31
32
 
32
33
  ### Git Status Icons
33
34
 
@@ -17,6 +17,7 @@ import { type GitStatusSummary, emptyGitStatus, readGitStatus } from "./git";
17
17
  import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
18
18
  import { type RuntimeInfo, readRuntimeInfo } from "./runtime";
19
19
  import { PolishedEditor } from "./ui";
20
+ import { installUserMessageStyle } from "./user-message";
20
21
 
21
22
  type FooterState = GitStatusSummary & {
22
23
  modelLabel: string;
@@ -120,12 +121,14 @@ export default function (pi: ExtensionAPI) {
120
121
  };
121
122
 
122
123
  let currentConfig: PolishedTuiConfig = loadConfig();
124
+ let activeTheme: Theme | undefined;
123
125
  let requestFooterRender: (() => void) | undefined;
124
126
  let stopProjectRefreshInterval: StopProjectRefreshInterval = () => {};
125
127
  let projectRefreshInFlight = false;
126
128
  let projectRefreshPending = false;
127
129
 
128
130
  const refresh = () => requestFooterRender?.();
131
+ const getActiveTheme = () => activeTheme;
129
132
 
130
133
  const cleanupUi = (ctx?: ExtensionContext) => {
131
134
  stopProjectRefreshInterval();
@@ -137,6 +140,7 @@ export default function (pi: ExtensionAPI) {
137
140
  ctx.ui.setFooter(undefined);
138
141
  ctx.ui.setEditorComponent(undefined);
139
142
  }
143
+ activeTheme = undefined;
140
144
  };
141
145
 
142
146
  const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
@@ -289,6 +293,8 @@ export default function (pi: ExtensionAPI) {
289
293
 
290
294
  const installUi = (ctx: ExtensionContext) => {
291
295
  if (!ctx.hasUI) return;
296
+ activeTheme = ctx.ui.theme;
297
+ installUserMessageStyle(getActiveTheme);
292
298
  ensureConfigExists();
293
299
  currentConfig = loadConfig();
294
300
  installFooter(ctx);
@@ -0,0 +1,133 @@
1
+ import { type Theme, type ThemeColor, UserMessageComponent } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ Markdown,
4
+ type MarkdownTheme,
5
+ truncateToWidth,
6
+ visibleWidth,
7
+ } from "@earendil-works/pi-tui";
8
+
9
+ const OSC133_ZONE_START = "\x1b]133;A\x07";
10
+ const OSC133_ZONE_END = "\x1b]133;B\x07";
11
+ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
12
+
13
+ type RenderFn = (width: number) => string[];
14
+
15
+ type PatchableUserMessagePrototype = {
16
+ render: RenderFn;
17
+ children?: unknown[];
18
+ __zentuiUserMessageOriginalRender?: RenderFn;
19
+ __zentuiUserMessagePatched?: boolean;
20
+ __zentuiUserMessageGetTheme?: () => Theme | undefined;
21
+ };
22
+
23
+ type MarkdownLike = {
24
+ text?: unknown;
25
+ };
26
+
27
+ function isRecord(value: unknown): value is Record<string, unknown> {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+
31
+ function findMarkdownText(value: unknown): string | undefined {
32
+ if (isRecord(value) && typeof (value as MarkdownLike).text === "string") {
33
+ return (value as { text: string }).text;
34
+ }
35
+
36
+ if (!isRecord(value)) return undefined;
37
+
38
+ const children = Array.isArray(value.children) ? value.children : [];
39
+ for (const child of children) {
40
+ const text = findMarkdownText(child);
41
+ if (text !== undefined) return text;
42
+ }
43
+
44
+ return undefined;
45
+ }
46
+
47
+ function themeFg(theme: Theme | undefined, color: ThemeColor, text: string): string {
48
+ return theme ? theme.fg(color, text) : text;
49
+ }
50
+
51
+ function makeMarkdownTheme(theme: Theme | undefined): MarkdownTheme {
52
+ return {
53
+ heading: (text) => themeFg(theme, "mdHeading", text),
54
+ link: (text) => themeFg(theme, "mdLink", text),
55
+ linkUrl: (text) => themeFg(theme, "mdLinkUrl", text),
56
+ code: (text) => themeFg(theme, "mdCode", text),
57
+ codeBlock: (text) => themeFg(theme, "mdCodeBlock", text),
58
+ codeBlockBorder: (text) => themeFg(theme, "mdCodeBlockBorder", text),
59
+ quote: (text) => themeFg(theme, "mdQuote", text),
60
+ quoteBorder: (text) => themeFg(theme, "mdQuoteBorder", text),
61
+ hr: (text) => themeFg(theme, "mdHr", text),
62
+ listBullet: (text) => themeFg(theme, "mdListBullet", text),
63
+ bold: (text) => (theme ? theme.bold(text) : text),
64
+ italic: (text) => (theme ? theme.italic(text) : text),
65
+ underline: (text) => (theme ? theme.underline(text) : text),
66
+ strikethrough: (text) => (theme ? theme.strikethrough(text) : text),
67
+ };
68
+ }
69
+
70
+ function fillLine(content: string, width: number): string {
71
+ const truncated = truncateToWidth(content, Math.max(0, width), "");
72
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated)));
73
+ return `${truncated}${pad}`;
74
+ }
75
+
76
+ function renderPromptBoxLine(line: string, width: number, theme: Theme | undefined): string {
77
+ if (width <= 0) return "";
78
+ const rail = `${themeFg(theme, "accent", "│")} `;
79
+ const contentWidth = Math.max(0, width - visibleWidth(rail));
80
+ return truncateToWidth(`${rail}${fillLine(line, contentWidth)}`, width, "");
81
+ }
82
+
83
+ function renderZentuiUserMessage(
84
+ instance: PatchableUserMessagePrototype,
85
+ width: number,
86
+ theme: Theme | undefined,
87
+ ): string[] | undefined {
88
+ const text = findMarkdownText(instance);
89
+ if (text === undefined) return undefined;
90
+ if (width <= 0) return [""];
91
+
92
+ const railWidth = visibleWidth(`${themeFg(theme, "accent", "│")} `);
93
+ const contentWidth = Math.max(1, width - railWidth);
94
+ const renderer = new Markdown(text, 0, 0, makeMarkdownTheme(theme), {
95
+ color: (content) => themeFg(theme, "userMessageText", content),
96
+ });
97
+ const body = renderer.render(contentWidth);
98
+ const contentLines = body.length > 0 ? body : [""];
99
+ const border = themeFg(theme, "border", "─".repeat(width));
100
+
101
+ return [
102
+ truncateToWidth(border, width, ""),
103
+ renderPromptBoxLine("", width, theme),
104
+ ...contentLines.map((line) => renderPromptBoxLine(line, width, theme)),
105
+ renderPromptBoxLine("", width, theme),
106
+ truncateToWidth(border, width, ""),
107
+ ];
108
+ }
109
+
110
+ export function installUserMessageStyle(getTheme: () => Theme | undefined): void {
111
+ const prototype = UserMessageComponent.prototype as unknown as PatchableUserMessagePrototype;
112
+ prototype.__zentuiUserMessageGetTheme = getTheme;
113
+
114
+ if (prototype.__zentuiUserMessagePatched) return;
115
+
116
+ prototype.__zentuiUserMessageOriginalRender = prototype.render;
117
+ prototype.render = function renderWithZentuiUserMessage(width: number): string[] {
118
+ const original = prototype.__zentuiUserMessageOriginalRender ?? prototype.render;
119
+ const lines = renderZentuiUserMessage(
120
+ this as PatchableUserMessagePrototype,
121
+ width,
122
+ prototype.__zentuiUserMessageGetTheme?.(),
123
+ );
124
+
125
+ if (!lines) return original.call(this, width);
126
+ if (lines.length === 0) return lines;
127
+
128
+ lines[0] = OSC133_ZONE_START + lines[0];
129
+ lines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];
130
+ return lines;
131
+ };
132
+ prototype.__zentuiUserMessagePatched = true;
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",