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 +1 -0
- package/extensions/zentui/index.ts +6 -0
- package/extensions/zentui/user-message.ts +133 -0
- package/package.json +1 -1
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
|
+
}
|