@leo-alvarenga/pi-zen-frame 0.11.0
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/LICENSE +21 -0
- package/README.md +148 -0
- package/docs/preview.png +0 -0
- package/extensions/components/agent-mode.ts +25 -0
- package/extensions/components/cwd.ts +31 -0
- package/extensions/components/frame.ts +140 -0
- package/extensions/components/header.ts +192 -0
- package/extensions/components/model.ts +16 -0
- package/extensions/components/reasoning.ts +22 -0
- package/extensions/components/registry.ts +42 -0
- package/extensions/components/spinner.ts +24 -0
- package/extensions/components/token-count.ts +55 -0
- package/extensions/components/types.ts +106 -0
- package/extensions/config/constants.ts +279 -0
- package/extensions/config/settings.ts +150 -0
- package/extensions/config/types.ts +115 -0
- package/extensions/editor/frame-editor.ts +197 -0
- package/extensions/index.ts +261 -0
- package/extensions/utils/agent.ts +43 -0
- package/extensions/utils/git.ts +32 -0
- package/extensions/utils/index.ts +16 -0
- package/extensions/utils/path.ts +18 -0
- package/extensions/utils/string.ts +6 -0
- package/package.json +42 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { SegmentDef } from "./types";
|
|
4
|
+
|
|
5
|
+
function trimFixed1(n: number): string {
|
|
6
|
+
const t = n.toFixed(1);
|
|
7
|
+
return t.endsWith(".0") ? t.slice(0, -2) : t;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatTokens(n: number): string {
|
|
11
|
+
if (n >= 1_000_000) return `${trimFixed1(n / 1_000_000)}M`;
|
|
12
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`;
|
|
13
|
+
|
|
14
|
+
return `${n}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatWindow(n: number): string {
|
|
18
|
+
if (n >= 1_000_000) return `${trimFixed1(n / 1_000_000)}M`;
|
|
19
|
+
return `${(n / 1_000).toFixed(0)}k`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Bottom-left: context window usage (percent + used/window tokens). */
|
|
23
|
+
export const tokenCountSegment: SegmentDef = {
|
|
24
|
+
id: "token-count",
|
|
25
|
+
slot: "topRight",
|
|
26
|
+
enabled: (_d, cfg) => cfg.showContext !== false,
|
|
27
|
+
render: (d, { border, theme, icons, segColor }) => {
|
|
28
|
+
const c = d.context;
|
|
29
|
+
if (!c) return "";
|
|
30
|
+
|
|
31
|
+
const pct = c.percent === null ? "?" : `${Math.round(c.percent)}%`;
|
|
32
|
+
let color: ThemeColor = "muted";
|
|
33
|
+
|
|
34
|
+
if (c.percent !== null) {
|
|
35
|
+
if (c.percent >= 80) {
|
|
36
|
+
color = "error";
|
|
37
|
+
} else if (c.percent >= 50) {
|
|
38
|
+
color = "warning";
|
|
39
|
+
} else {
|
|
40
|
+
color = "success";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const used = c.tokens === null ? "?" : formatTokens(c.tokens);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
theme.fg(segColor("context", color), ` ${icons.context} ctx ${pct} `) +
|
|
48
|
+
border("·") +
|
|
49
|
+
theme.fg(
|
|
50
|
+
segColor("context", color),
|
|
51
|
+
` ${used}/${formatWindow(c.window)} `,
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE COMPONENT CONTRACT.
|
|
3
|
+
*
|
|
4
|
+
* A "component" here is a small, isolated piece of the editor frame: Model,
|
|
5
|
+
* Reasoning, spinner, token count, cwd, agent mode, ... Each lives
|
|
6
|
+
* in its own file, exports a `SegmentDef`, and is registered in registry.ts.
|
|
7
|
+
*
|
|
8
|
+
* Adding a new component = 1 new file + 1 line in the registry. The frame
|
|
9
|
+
* does all width math, joining and truncation — a segment never sees widths.
|
|
10
|
+
*/
|
|
11
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
FrameColors,
|
|
15
|
+
FrameIcons,
|
|
16
|
+
FrameSettings,
|
|
17
|
+
SpinnerPhase,
|
|
18
|
+
} from "../config/types";
|
|
19
|
+
|
|
20
|
+
/** Where in the frame border a segment renders. */
|
|
21
|
+
export type Slot = "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
|
|
22
|
+
|
|
23
|
+
export type AgentConfig = {
|
|
24
|
+
name: string;
|
|
25
|
+
icon?: string;
|
|
26
|
+
color?: ThemeColor;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AgentState = {
|
|
30
|
+
currentAgent: string;
|
|
31
|
+
currentAgentLabel: string;
|
|
32
|
+
currentAgentConfig: AgentConfig;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Agent-mode state surfaced by pi-agent-manager. */
|
|
36
|
+
export type AgentMode = {
|
|
37
|
+
name: string;
|
|
38
|
+
icon?: string;
|
|
39
|
+
color?: ThemeColor;
|
|
40
|
+
} | null;
|
|
41
|
+
|
|
42
|
+
/** Live snapshot of everything a segment may want. Rebuilt on every paint. */
|
|
43
|
+
export interface FrameData {
|
|
44
|
+
cwd: string;
|
|
45
|
+
gitDirty: number;
|
|
46
|
+
spinnerFrame: string;
|
|
47
|
+
accentColor: ThemeColor;
|
|
48
|
+
gitBranch: string | undefined;
|
|
49
|
+
modelName: string | undefined;
|
|
50
|
+
spinnerPhase: SpinnerPhase | null;
|
|
51
|
+
thinkingLevel: string | undefined;
|
|
52
|
+
|
|
53
|
+
/** Master mute: when true, every segment except agentMode renders muted. */
|
|
54
|
+
zenMode: boolean;
|
|
55
|
+
|
|
56
|
+
/** To render the agent mode (from @leo-alvarenga/pi-agent-manager) segment, if any */
|
|
57
|
+
agentMode: AgentMode;
|
|
58
|
+
|
|
59
|
+
context: {
|
|
60
|
+
window: number;
|
|
61
|
+
tokens: number | null;
|
|
62
|
+
percent: number | null;
|
|
63
|
+
} | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Data + rendering helpers handed to every segment. */
|
|
67
|
+
export interface SegmentContext {
|
|
68
|
+
theme: Theme;
|
|
69
|
+
icons: FrameIcons;
|
|
70
|
+
cfg: FrameSettings;
|
|
71
|
+
border: (str: string) => string;
|
|
72
|
+
|
|
73
|
+
/** Resolve a segment's fg: zen-mode mutes everything except agentMode;
|
|
74
|
+
* else `frame.colors.<key>` supersedes the segment default. */
|
|
75
|
+
segColor: (key: keyof FrameColors, fallback: ThemeColor) => ThemeColor;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A segment renders an already-ANSI-styled string, or "" to render nothing. */
|
|
79
|
+
export type Segment = (d: FrameData, ctx: SegmentContext) => string;
|
|
80
|
+
|
|
81
|
+
export interface SegmentDef {
|
|
82
|
+
id: string;
|
|
83
|
+
slot: Slot;
|
|
84
|
+
render: Segment;
|
|
85
|
+
enabled?: (d: FrameData, cfg: FrameSettings) => boolean;
|
|
86
|
+
|
|
87
|
+
/** When enabled, this segment replaces everything else in the slot. */
|
|
88
|
+
replaces?: (d: FrameData, cfg: FrameSettings) => boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Non-editor state the provider assembles on every render. The editor merges
|
|
93
|
+
* this with its own live state (mode / count / spinner frame) in render().
|
|
94
|
+
*/
|
|
95
|
+
export interface ExternalData {
|
|
96
|
+
modelName: string | undefined;
|
|
97
|
+
thinkingLevel: string | undefined;
|
|
98
|
+
spinnerPhase: SpinnerPhase | null;
|
|
99
|
+
context: FrameData["context"];
|
|
100
|
+
cwd: string;
|
|
101
|
+
gitBranch: string | undefined;
|
|
102
|
+
gitDirty: number;
|
|
103
|
+
agentMode: AgentMode;
|
|
104
|
+
theme: Theme | undefined;
|
|
105
|
+
zenMode: boolean;
|
|
106
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { BannerTip, FrameIcons, Settings, SpinnerPhase } from "./types";
|
|
3
|
+
|
|
4
|
+
export const CONFIG_FILE_NAME = "pi-zen-frame.json";
|
|
5
|
+
|
|
6
|
+
/** pi-agent-manager integration (optional — no hard dependency). */
|
|
7
|
+
export const PI_AGENT_MANAGER_AGENT_EVENT = "pi-agent-manager:agent-changed";
|
|
8
|
+
export const PI_AGENT_MANAGER_AGENT_DATA_KEY = "pi-agent-manager-agent";
|
|
9
|
+
|
|
10
|
+
/** Keybinding id users bind in keybindings.json to toggle zen mode. */
|
|
11
|
+
export const ZEN_MODE_SHORTCUT_ID = "piZenFrame.zenMode";
|
|
12
|
+
/** Default key when the user hasn't bound one (override or disable via
|
|
13
|
+
* keybindings.json; `[]` disables the shortcut, `/zen_mode` still works). */
|
|
14
|
+
export const ZEN_MODE_DEFAULT_KEY = "ctrl+shift+z";
|
|
15
|
+
|
|
16
|
+
/** Spinner animation frames per phase (pi-editor-shell style). */
|
|
17
|
+
export const SPINNER_FRAMES: Record<SpinnerPhase, string[]> = {
|
|
18
|
+
idle: ["⠃", "⠞", "⡵", "⠿", "⢹", "⠄"],
|
|
19
|
+
outputting: ["⠋", "⠙", "⠸", "⠴", "⠦", "⠇", "⠏"],
|
|
20
|
+
thinking: ["", "", "", "", "", "", "", ""],
|
|
21
|
+
toolcall: ["●", "●", "●", "●", "○", "○", "○", "○"],
|
|
22
|
+
exec: ["⠂", "⠅", "⠍", "⠟", "⠿", "⠽", "⠿", "⠟", "⠍", "⠅", "⠂"],
|
|
23
|
+
|
|
24
|
+
// Sample ["░", "▒", "▓", "█", "▓", "▒"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Nerd Font defaults (override any subset via `frame.icons`). */
|
|
28
|
+
export const DEFAULT_ICONS: FrameIcons = {
|
|
29
|
+
model: "",
|
|
30
|
+
folder: " ",
|
|
31
|
+
context: "",
|
|
32
|
+
gitDirty: "",
|
|
33
|
+
thinking: "",
|
|
34
|
+
gitBranch: "",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Thinking level → theme token, mirroring pi's own border-color mapping. */
|
|
38
|
+
export const THINKING_TOKEN: Record<string, string> = {
|
|
39
|
+
off: "thinkingOff",
|
|
40
|
+
minimal: "thinkingMinimal",
|
|
41
|
+
low: "thinkingLow",
|
|
42
|
+
medium: "thinkingMedium",
|
|
43
|
+
high: "thinkingHigh",
|
|
44
|
+
xhigh: "thinkingXhigh",
|
|
45
|
+
max: "thinkingMax",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Rotating messages for the working loader (single-word verbs; some silly, some plausible). */
|
|
49
|
+
export const WORKING_MESSAGES: string[] = [
|
|
50
|
+
"Exploring",
|
|
51
|
+
"Tinkering",
|
|
52
|
+
"Searching",
|
|
53
|
+
"Polishing",
|
|
54
|
+
"Herding",
|
|
55
|
+
"Consulting",
|
|
56
|
+
"Chasing",
|
|
57
|
+
"Rearranging",
|
|
58
|
+
"Sharpening",
|
|
59
|
+
"Reticulating",
|
|
60
|
+
"Warming",
|
|
61
|
+
"Feeding",
|
|
62
|
+
"Whispering",
|
|
63
|
+
"Counting",
|
|
64
|
+
"Dusting",
|
|
65
|
+
"Tightening",
|
|
66
|
+
"Watering",
|
|
67
|
+
"Aligning",
|
|
68
|
+
"Reading",
|
|
69
|
+
"Charging",
|
|
70
|
+
"Summoning",
|
|
71
|
+
"Calibrating",
|
|
72
|
+
"Sorting",
|
|
73
|
+
"Debating",
|
|
74
|
+
"Brewing",
|
|
75
|
+
"Stargazing",
|
|
76
|
+
"Walking",
|
|
77
|
+
"Folding",
|
|
78
|
+
"Rounding",
|
|
79
|
+
"Cooking",
|
|
80
|
+
"Crunching",
|
|
81
|
+
"Cogitating",
|
|
82
|
+
"Analyzing",
|
|
83
|
+
"Architecting",
|
|
84
|
+
"Assembling",
|
|
85
|
+
"Building",
|
|
86
|
+
"Calculating",
|
|
87
|
+
"Compiling",
|
|
88
|
+
"Composing",
|
|
89
|
+
"Crafting",
|
|
90
|
+
"Decoding",
|
|
91
|
+
"Designing",
|
|
92
|
+
"Drafting",
|
|
93
|
+
"Editing",
|
|
94
|
+
"Evaluating",
|
|
95
|
+
"Experimenting",
|
|
96
|
+
"Explaining",
|
|
97
|
+
"Extracting",
|
|
98
|
+
"Generating",
|
|
99
|
+
"Integrating",
|
|
100
|
+
"Iterating",
|
|
101
|
+
"Juggling",
|
|
102
|
+
"Mapping",
|
|
103
|
+
"Measuring",
|
|
104
|
+
"Merging",
|
|
105
|
+
"Modeling",
|
|
106
|
+
"Navigating",
|
|
107
|
+
"Optimizing",
|
|
108
|
+
"Organizing",
|
|
109
|
+
"Parsing",
|
|
110
|
+
"Planning",
|
|
111
|
+
"Pondering",
|
|
112
|
+
"Processing",
|
|
113
|
+
"Refactoring",
|
|
114
|
+
"Refining",
|
|
115
|
+
"Rendering",
|
|
116
|
+
"Resolving",
|
|
117
|
+
"Reviewing",
|
|
118
|
+
"Scanning",
|
|
119
|
+
"Sifting",
|
|
120
|
+
"Simulating",
|
|
121
|
+
"Streamlining",
|
|
122
|
+
"Structuring",
|
|
123
|
+
"Synthesizing",
|
|
124
|
+
"Testing",
|
|
125
|
+
"Tracing",
|
|
126
|
+
"Translating",
|
|
127
|
+
"Triaging",
|
|
128
|
+
"Unraveling",
|
|
129
|
+
"Validating",
|
|
130
|
+
"Verifying",
|
|
131
|
+
"Wrangling",
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
export const HEADER_TIPS: BannerTip[] = [
|
|
135
|
+
{
|
|
136
|
+
type: "Tip",
|
|
137
|
+
text: "Press Ctrl + G inside Pi's input box to open your default system $EDITOR (e.g., Neovim) for writing long, complex prompts",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: "Did You Know?",
|
|
141
|
+
text: "Model reasoning quality degrades as the context window fills up. Use /compact or start fresh sessions once context exceeds 50–60%",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: "Workflow",
|
|
145
|
+
text: "Isolate planning from execution: use a read-only profile for architectural planning and switch to full permissions only when building",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: "Did You Know?",
|
|
149
|
+
text: "Modern models like Claude 3.5 and 3.7 process boundaries in XML tags (<context>, <rules>, <code_diff>) significantly better than raw Markdown dividers",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "Tip",
|
|
153
|
+
text: "Always truncate or filter large log outputs before handing them to an agent—a 10,000-line stack trace will immediately saturate your token cache",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: "Tip",
|
|
157
|
+
text: "Export global environment variables (like $EDITOR and $VISUAL) in ~/.bashenv (or your shell's env file) so background subshells and GUI-spawned processes inherit them cleanly",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: "Did You Know?",
|
|
161
|
+
text: "Smooth 80ms Braille sequences (⠋, ⠙, ⠹, ⠸) or quadrant block meters give immediate visual feedback without consuming much screen real estate",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: "Prompting",
|
|
165
|
+
text: "Tell models what TO DO instead of what NOT to do—negative constraints like 'don't use markdown' are frequently ignored compared to positive instructions",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: "Workflow",
|
|
169
|
+
text: "Use fuzzy file references by typing `@` in Pi's input box to quickly anchor specific codebase files directly into the prompt context",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: "Tip",
|
|
173
|
+
text: "Place heavy longform context or data files near the top of your prompt and put your actual instructions or questions at the very end for better accuracy",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: "Did You Know?",
|
|
177
|
+
text: "Pi saves session history as a tree. You can use `/tree` or `/fork` to branch off a previous point in a conversation to test an alternative approach",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
type: "Prompting",
|
|
181
|
+
text: "Define specific output formats (e.g., JSON schemas or concise bullet points) upfront to prevent models from generating unnecessary verbose prose",
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
export const DEFAULT_SETTINGS: Settings = {
|
|
186
|
+
zenMode: false,
|
|
187
|
+
accentColor: "accent",
|
|
188
|
+
|
|
189
|
+
header: {
|
|
190
|
+
enable: false,
|
|
191
|
+
logoColor: "text",
|
|
192
|
+
accentColor: "accent",
|
|
193
|
+
|
|
194
|
+
heading: "Welcome back!",
|
|
195
|
+
subheading:
|
|
196
|
+
"Ready for your next session? Terminal warm, context clean, tools ready to execute",
|
|
197
|
+
|
|
198
|
+
logo: ["█████████ ", "███ ███ ", "██████ ", "███ ███"],
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
workingMessage: {
|
|
202
|
+
enable: true,
|
|
203
|
+
intervalMs: 3000,
|
|
204
|
+
messages: WORKING_MESSAGES,
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
frame: {
|
|
208
|
+
icons: {},
|
|
209
|
+
paddingX: 1,
|
|
210
|
+
enable: true,
|
|
211
|
+
minWidth: 20,
|
|
212
|
+
marginTop: 0,
|
|
213
|
+
showCwd: true,
|
|
214
|
+
paddingTop: 1,
|
|
215
|
+
marginBottom: 0,
|
|
216
|
+
showModel: true,
|
|
217
|
+
paddingBottom: 1,
|
|
218
|
+
showSpinner: false,
|
|
219
|
+
showContext: true,
|
|
220
|
+
showThinking: true,
|
|
221
|
+
showAgentMode: true,
|
|
222
|
+
borderColor: "text",
|
|
223
|
+
|
|
224
|
+
colors: {},
|
|
225
|
+
// ponytail: colors were dormant (nothing read them); now wired — keep
|
|
226
|
+
// defaults unset so segments keep their natural colors and zen-mode is
|
|
227
|
+
// the mute switch.
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const THEME_COLORS: Record<ThemeColor, true> = {
|
|
232
|
+
accent: true,
|
|
233
|
+
border: true,
|
|
234
|
+
borderAccent: true,
|
|
235
|
+
borderMuted: true,
|
|
236
|
+
success: true,
|
|
237
|
+
error: true,
|
|
238
|
+
warning: true,
|
|
239
|
+
muted: true,
|
|
240
|
+
dim: true,
|
|
241
|
+
text: true,
|
|
242
|
+
searchMatchText: true,
|
|
243
|
+
thinkingText: true,
|
|
244
|
+
userMessageText: true,
|
|
245
|
+
customMessageText: true,
|
|
246
|
+
customMessageLabel: true,
|
|
247
|
+
toolTitle: true,
|
|
248
|
+
toolOutput: true,
|
|
249
|
+
mdHeading: true,
|
|
250
|
+
mdLink: true,
|
|
251
|
+
mdLinkUrl: true,
|
|
252
|
+
mdCode: true,
|
|
253
|
+
mdCodeBlock: true,
|
|
254
|
+
mdCodeBlockBorder: true,
|
|
255
|
+
mdQuote: true,
|
|
256
|
+
mdQuoteBorder: true,
|
|
257
|
+
mdHr: true,
|
|
258
|
+
mdListBullet: true,
|
|
259
|
+
toolDiffAdded: true,
|
|
260
|
+
toolDiffRemoved: true,
|
|
261
|
+
toolDiffContext: true,
|
|
262
|
+
syntaxComment: true,
|
|
263
|
+
syntaxKeyword: true,
|
|
264
|
+
syntaxFunction: true,
|
|
265
|
+
syntaxVariable: true,
|
|
266
|
+
syntaxString: true,
|
|
267
|
+
syntaxNumber: true,
|
|
268
|
+
syntaxType: true,
|
|
269
|
+
syntaxOperator: true,
|
|
270
|
+
syntaxPunctuation: true,
|
|
271
|
+
thinkingOff: true,
|
|
272
|
+
thinkingMinimal: true,
|
|
273
|
+
thinkingLow: true,
|
|
274
|
+
thinkingMedium: true,
|
|
275
|
+
thinkingHigh: true,
|
|
276
|
+
thinkingXhigh: true,
|
|
277
|
+
thinkingMax: true,
|
|
278
|
+
bashMode: true,
|
|
279
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CONFIG_FILE_NAME,
|
|
8
|
+
DEFAULT_SETTINGS,
|
|
9
|
+
WORKING_MESSAGES,
|
|
10
|
+
} from "./constants";
|
|
11
|
+
import { Settings, FrameColors } from "./types";
|
|
12
|
+
import { isThemeColor } from "../utils";
|
|
13
|
+
|
|
14
|
+
function getResolvedSettingsFilePath(): string {
|
|
15
|
+
return join(getAgentDir(), CONFIG_FILE_NAME);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadSettingsFile(): Promise<string> {
|
|
19
|
+
return readFile(getResolvedSettingsFilePath(), "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Validate a raw (possibly malformed) config against the schema shapes. */
|
|
23
|
+
function normalize(raw: unknown): Settings {
|
|
24
|
+
if (!raw || typeof raw !== "object") return DEFAULT_SETTINGS;
|
|
25
|
+
|
|
26
|
+
const num = (v: unknown, fallback: number): number =>
|
|
27
|
+
typeof v === "number" && Number.isFinite(v) && v >= 0
|
|
28
|
+
? Math.floor(v)
|
|
29
|
+
: fallback;
|
|
30
|
+
|
|
31
|
+
const bool = (v: unknown, fallback: boolean | undefined): boolean =>
|
|
32
|
+
typeof v === "boolean" ? v : (fallback ?? true);
|
|
33
|
+
|
|
34
|
+
const str = (v: unknown, fallback: string): string =>
|
|
35
|
+
typeof v === "string" ? v : fallback;
|
|
36
|
+
|
|
37
|
+
const d = DEFAULT_SETTINGS;
|
|
38
|
+
const r = raw as Record<string, unknown>;
|
|
39
|
+
|
|
40
|
+
const out: Settings = {
|
|
41
|
+
...DEFAULT_SETTINGS,
|
|
42
|
+
frame: { ...DEFAULT_SETTINGS.frame },
|
|
43
|
+
zenMode: bool(r.zenMode, d.zenMode ?? true),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (typeof r.frame === "object" && r.frame) {
|
|
47
|
+
const f = r.frame as Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
out.frame = {
|
|
50
|
+
enable: bool(f.enable, d.frame?.enable),
|
|
51
|
+
minWidth: num(f.minWidth, d.frame?.minWidth ?? 20),
|
|
52
|
+
paddingTop: num(f.paddingTop, d.frame?.paddingTop ?? 1),
|
|
53
|
+
paddingBottom: num(f.paddingBottom, d.frame?.paddingBottom ?? 1),
|
|
54
|
+
paddingX: num(f.paddingX, d.frame?.paddingX ?? 1),
|
|
55
|
+
marginTop: num(f.marginTop, d.frame?.marginTop ?? 0),
|
|
56
|
+
marginBottom: num(f.marginBottom, d.frame?.marginBottom ?? 0),
|
|
57
|
+
showModel: bool(f.showModel, d.frame?.showModel),
|
|
58
|
+
showThinking: bool(f.showThinking, d.frame?.showThinking),
|
|
59
|
+
showContext: bool(f.showContext, d.frame?.showContext),
|
|
60
|
+
showCwd: bool(f.showCwd, d.frame?.showCwd),
|
|
61
|
+
showAgentMode: bool(f.showAgentMode, d.frame?.showAgentMode),
|
|
62
|
+
showSpinner: bool(f.showSpinner, d.frame?.showSpinner),
|
|
63
|
+
icons:
|
|
64
|
+
typeof f.icons === "object" && f.icons
|
|
65
|
+
? { ...d.frame?.icons, ...(f.icons as Record<string, unknown>) }
|
|
66
|
+
: d.frame?.icons,
|
|
67
|
+
borderColor:
|
|
68
|
+
typeof f.borderColor === "string" &&
|
|
69
|
+
(isThemeColor(f.borderColor) || f.borderColor === "agentMode")
|
|
70
|
+
? f.borderColor
|
|
71
|
+
: (d.frame?.borderColor ?? "border"),
|
|
72
|
+
|
|
73
|
+
colors:
|
|
74
|
+
typeof f.colors === "object" && f.colors
|
|
75
|
+
? (Object.fromEntries(
|
|
76
|
+
Object.entries(f.colors as Record<string, unknown>).filter(
|
|
77
|
+
([, v]) => typeof v === "string" && isThemeColor(v),
|
|
78
|
+
),
|
|
79
|
+
) as Partial<FrameColors>)
|
|
80
|
+
: d.frame?.colors,
|
|
81
|
+
|
|
82
|
+
prefix:
|
|
83
|
+
typeof f.prefix === "string" ? f.prefix : (d.frame?.prefix ?? "❯"),
|
|
84
|
+
|
|
85
|
+
prefixColor:
|
|
86
|
+
typeof f.prefixColor === "string" &&
|
|
87
|
+
(f.prefixColor === "agentMode" ||
|
|
88
|
+
f.prefixColor === "frameBorder" ||
|
|
89
|
+
isThemeColor(f.prefixColor))
|
|
90
|
+
? f.prefixColor
|
|
91
|
+
: d.frame?.prefixColor, // unset → text
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof r.header === "object" && r.header) {
|
|
96
|
+
const h = r.header as Record<string, unknown>;
|
|
97
|
+
|
|
98
|
+
out.header = {
|
|
99
|
+
enable: bool(h.enable, d.header?.enable),
|
|
100
|
+
heading: str(h.heading, d.header?.heading ?? ""),
|
|
101
|
+
subheading: str(h.subheading, d.header?.subheading ?? ""),
|
|
102
|
+
logoColor:
|
|
103
|
+
typeof h.logoColor === "string" && isThemeColor(h.logoColor)
|
|
104
|
+
? h.logoColor
|
|
105
|
+
: (d.header?.logoColor ?? "accent"),
|
|
106
|
+
accentColor:
|
|
107
|
+
typeof h.accentColor === "string" && isThemeColor(h.accentColor)
|
|
108
|
+
? h.accentColor
|
|
109
|
+
: (d.header?.accentColor ?? "accent"),
|
|
110
|
+
logo:
|
|
111
|
+
Array.isArray(h.logo) && h.logo.every((x) => typeof x === "string")
|
|
112
|
+
? (h.logo as string[])
|
|
113
|
+
: (d.header?.logo ?? []),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof r.accentColor === "string" && isThemeColor(r.accentColor)) {
|
|
118
|
+
out.accentColor = r.accentColor || d.accentColor;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof r.workingMessage === "object" && r.workingMessage) {
|
|
122
|
+
const wm = r.workingMessage as Record<string, unknown>;
|
|
123
|
+
|
|
124
|
+
out.workingMessage = {
|
|
125
|
+
enable:
|
|
126
|
+
typeof wm.enable === "boolean"
|
|
127
|
+
? wm.enable
|
|
128
|
+
: (d.workingMessage?.enable ?? true),
|
|
129
|
+
intervalMs: num(wm.intervalMs, d.workingMessage?.intervalMs ?? 3000),
|
|
130
|
+
messages:
|
|
131
|
+
Array.isArray(wm.messages) &&
|
|
132
|
+
wm.messages.length > 0 &&
|
|
133
|
+
wm.messages.every((m) => typeof m === "string")
|
|
134
|
+
? (wm.messages as string[])
|
|
135
|
+
: (d.workingMessage?.messages ??
|
|
136
|
+
(wm?.enable === true ? WORKING_MESSAGES : [])),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function loadSettings(): Promise<Settings> {
|
|
144
|
+
try {
|
|
145
|
+
const raw = JSON.parse(await loadSettingsFile()) as unknown;
|
|
146
|
+
return normalize(raw);
|
|
147
|
+
} catch {
|
|
148
|
+
return DEFAULT_SETTINGS;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** Streaming phase used to drive the status-animation spinner. */
|
|
4
|
+
export type SpinnerPhase =
|
|
5
|
+
"thinking" | "outputting" | "toolcall" | "exec" | "idle";
|
|
6
|
+
|
|
7
|
+
export interface BannerTip {
|
|
8
|
+
type: "Tip" | "Did You Know?" | "Workflow" | "Prompting";
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FrameColors {
|
|
13
|
+
border: ThemeColor;
|
|
14
|
+
background: ThemeColor;
|
|
15
|
+
cwd: ThemeColor;
|
|
16
|
+
model: ThemeColor;
|
|
17
|
+
context: ThemeColor;
|
|
18
|
+
thinking: ThemeColor;
|
|
19
|
+
agentMode: ThemeColor;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Nerd-font / glyph icons shown in the frame border. Each is overridable. */
|
|
23
|
+
export interface FrameIcons {
|
|
24
|
+
folder: string;
|
|
25
|
+
model: string;
|
|
26
|
+
context: string;
|
|
27
|
+
thinking: string;
|
|
28
|
+
gitDirty: string;
|
|
29
|
+
gitBranch: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ``frame`` — the rounded-corner shell look. Padding values are plain
|
|
34
|
+
* numbers (>= 0): paddingTop/paddingBottom are blank lines shown *inside*
|
|
35
|
+
* the box above/below the content.
|
|
36
|
+
*/
|
|
37
|
+
export interface FrameSettings {
|
|
38
|
+
enable?: boolean;
|
|
39
|
+
|
|
40
|
+
/** Below this terminal width the frame is skipped entirely (passthrough) */
|
|
41
|
+
minWidth?: number;
|
|
42
|
+
|
|
43
|
+
/** Blank lines inside the box above the content. Default 1. */
|
|
44
|
+
paddingTop?: number;
|
|
45
|
+
|
|
46
|
+
/** Blank lines inside the box below the content. Default 1. */
|
|
47
|
+
paddingBottom?: number;
|
|
48
|
+
|
|
49
|
+
/** Horizontal inner padding for the content rows. Default 1. */
|
|
50
|
+
paddingX?: number;
|
|
51
|
+
|
|
52
|
+
/** Blank rows OUTSIDE the box, above its top border. Default 0. */
|
|
53
|
+
marginTop?: number;
|
|
54
|
+
|
|
55
|
+
/** Blank rows OUTSIDE the box, below its bottom border. Default 0. */
|
|
56
|
+
marginBottom?: number;
|
|
57
|
+
|
|
58
|
+
showCwd?: boolean;
|
|
59
|
+
showModel?: boolean;
|
|
60
|
+
showContext?: boolean;
|
|
61
|
+
showThinking?: boolean;
|
|
62
|
+
showAgentMode?: boolean;
|
|
63
|
+
|
|
64
|
+
/** Show the status-animation spinner segment while streaming. Default false. */
|
|
65
|
+
showSpinner?: boolean;
|
|
66
|
+
|
|
67
|
+
icons?: Partial<FrameIcons>;
|
|
68
|
+
colors?: Partial<FrameColors>;
|
|
69
|
+
|
|
70
|
+
/** ThemeColor used as the fg for all editor border characters; If set to `"agentMode"`, will follow the current Agent color. Default "border". */
|
|
71
|
+
borderColor?: ThemeColor | "agentMode";
|
|
72
|
+
|
|
73
|
+
/** Text shown before the editor content. Default "❯". */
|
|
74
|
+
prefix?: string;
|
|
75
|
+
|
|
76
|
+
/** ThemeColor used as the fg for the prefix text; If set to `"agentMode"`, will follow the current Agent color;
|
|
77
|
+
* If set to `"frameBorder"`, will follow the border color. Default: text color. */
|
|
78
|
+
prefixColor?: "agentMode" | "frameBorder" | ThemeColor;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `header` — the top-line header, which can show a logo or other text */
|
|
82
|
+
export interface HeaderSettings {
|
|
83
|
+
logo: string[];
|
|
84
|
+
enable: boolean;
|
|
85
|
+
heading: string;
|
|
86
|
+
subheading: string;
|
|
87
|
+
logoColor: ThemeColor;
|
|
88
|
+
accentColor: ThemeColor;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `workingMessage` — rotating messages in pi's built-in working loader. */
|
|
92
|
+
export interface WorkingMessageSettings {
|
|
93
|
+
/** Toggle the rotating messages. Default true. */
|
|
94
|
+
enable?: boolean;
|
|
95
|
+
|
|
96
|
+
/** How often (ms) the message is replaced. Default 3000. */
|
|
97
|
+
intervalMs?: number;
|
|
98
|
+
|
|
99
|
+
/** Custom message pool; replaces the default 30. */
|
|
100
|
+
messages?: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface Settings {
|
|
104
|
+
frame?: FrameSettings;
|
|
105
|
+
header?: HeaderSettings;
|
|
106
|
+
workingMessage?: WorkingMessageSettings;
|
|
107
|
+
|
|
108
|
+
/** Master mute: every segment except agent-mode renders muted.
|
|
109
|
+
* Default true. Toggle at runtime with `/zen_mode` or the `piZenFrame.zenMode`
|
|
110
|
+
* keybinding (default `ctrl+shift+z`). */
|
|
111
|
+
zenMode?: boolean;
|
|
112
|
+
|
|
113
|
+
/** Accent color for the frame border and other highlights; Defaults to 'accent' */
|
|
114
|
+
accentColor?: ThemeColor;
|
|
115
|
+
}
|