pi-one-ui 0.1.1
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 +106 -0
- package/extensions/app/config/renderer.ts +452 -0
- package/extensions/app/config/shell.ts +2484 -0
- package/extensions/app/ownership.ts +18 -0
- package/extensions/app/panel.ts +533 -0
- package/extensions/app/presets.ts +36 -0
- package/extensions/features/agent-summary/core.ts +194 -0
- package/extensions/features/agent-summary/index.ts +37 -0
- package/extensions/features/compact-thinking.ts +1262 -0
- package/extensions/features/context.ts +857 -0
- package/extensions/features/reference/index.ts +643 -0
- package/extensions/features/reference/session.ts +278 -0
- package/extensions/features/reference/subagent.ts +218 -0
- package/extensions/features/shell/aliases.ts +28 -0
- package/extensions/features/shell/flush-docked-bash.ts +39 -0
- package/extensions/features/shell/startup-header.ts +357 -0
- package/extensions/features/shell/working-message.ts +226 -0
- package/extensions/index.ts +52 -0
- package/extensions/shell/accent-rail-editor.ts +112 -0
- package/extensions/shell/accent-rail-layout-patch.ts +530 -0
- package/extensions/shell/completion-menu.ts +186 -0
- package/extensions/shell/editor-metadata-format.ts +287 -0
- package/extensions/shell/editor-transfer.ts +73 -0
- package/extensions/shell/extension-status.ts +102 -0
- package/extensions/shell/footer-format.ts +388 -0
- package/extensions/shell/footer-layout.ts +144 -0
- package/extensions/shell/footer.ts +1035 -0
- package/extensions/shell/format.ts +577 -0
- package/extensions/shell/git.ts +434 -0
- package/extensions/shell/icons.ts +261 -0
- package/extensions/shell/index.ts +1659 -0
- package/extensions/shell/interaction-summary.ts +1194 -0
- package/extensions/shell/live-context.ts +82 -0
- package/extensions/shell/minimalist-editor.ts +548 -0
- package/extensions/shell/package-version.ts +675 -0
- package/extensions/shell/project-refresh.ts +130 -0
- package/extensions/shell/project-state.ts +60 -0
- package/extensions/shell/prototype-patch-registry.ts +183 -0
- package/extensions/shell/runtime.ts +915 -0
- package/extensions/shell/selector-border.ts +109 -0
- package/extensions/shell/session-lifecycle.ts +60 -0
- package/extensions/shell/settings-command.ts +2144 -0
- package/extensions/shell/settings-previews.ts +137 -0
- package/extensions/shell/state.ts +83 -0
- package/extensions/shell/style.ts +480 -0
- package/extensions/shell/telemetry.ts +90 -0
- package/extensions/shell/ui.ts +1326 -0
- package/extensions/shell/user-message-osc.ts +279 -0
- package/extensions/shell/user-message-styles.ts +266 -0
- package/extensions/shell/user-message.ts +226 -0
- package/extensions/shell/working-line-messages.ts +24 -0
- package/extensions/shell/working-line-spinners.ts +54 -0
- package/extensions/shell/working-line.ts +1603 -0
- package/extensions/tools/ansi-color.ts +44 -0
- package/extensions/tools/ansi-text.ts +52 -0
- package/extensions/tools/component-tree.ts +37 -0
- package/extensions/tools/format.ts +28 -0
- package/extensions/tools/fullscreen-detect.ts +11 -0
- package/extensions/tools/patch-keys.ts +158 -0
- package/extensions/tools/sgr-mouse.ts +59 -0
- package/extensions/tools/tool-loading-icon.ts +20 -0
- package/extensions/tools/tool-result-sanitize.ts +27 -0
- package/extensions/transcript/index.ts +65 -0
- package/extensions/transcript/renderer/compact-mode.ts +1443 -0
- package/extensions/transcript/renderer/default-mode.ts +735 -0
- package/extensions/transcript/renderer/index.ts +286 -0
- package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
- package/extensions/transcript/renderer/mouse/hover.ts +175 -0
- package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
- package/extensions/transcript/renderer/mouse/layout.ts +207 -0
- package/extensions/transcript/renderer/mouse/packets.ts +96 -0
- package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
- package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
- package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
- package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
- package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
- package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
- package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
- package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
- package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
- package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
- package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
- package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
- package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
- package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
- package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
- package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
- package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
- package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
- package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
- package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
- package/extensions/transcript/renderer/tool/grouping.ts +619 -0
- package/extensions/transcript/renderer/tool/message-display.ts +183 -0
- package/extensions/transcript/renderer/tool/names.ts +195 -0
- package/extensions/transcript/renderer/tool/result.ts +842 -0
- package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
- package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
- package/package.json +58 -0
- package/themes/cc-dark.json +91 -0
- package/themes/cc-light.json +91 -0
|
@@ -0,0 +1,2144 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
type AutocompleteItem,
|
|
8
|
+
Key,
|
|
9
|
+
matchesKey,
|
|
10
|
+
type SettingItem,
|
|
11
|
+
SettingsList,
|
|
12
|
+
type SettingsListTheme,
|
|
13
|
+
truncateToWidth,
|
|
14
|
+
visibleWidth,
|
|
15
|
+
} from "@earendil-works/pi-tui";
|
|
16
|
+
import {
|
|
17
|
+
type AccentRailEditorStyleConfig,
|
|
18
|
+
type ColorSource,
|
|
19
|
+
type CompactFooterMaxLines,
|
|
20
|
+
type CompletionMenuStyle,
|
|
21
|
+
type ContextStyle,
|
|
22
|
+
type EditorBorderColorMode,
|
|
23
|
+
type EditorComponentConfig,
|
|
24
|
+
type EditorStyle,
|
|
25
|
+
type ExtensionStatusColorMode,
|
|
26
|
+
type ExtensionStatusPlacement,
|
|
27
|
+
type FooterComponentConfig,
|
|
28
|
+
type FooterSegmentsConfig,
|
|
29
|
+
type FooterStyle,
|
|
30
|
+
type GitBranchConfig,
|
|
31
|
+
type GitBranchMaxLength,
|
|
32
|
+
type GitCommitConfig,
|
|
33
|
+
type GitMetricsConfig,
|
|
34
|
+
getExtensionStatusColorMode,
|
|
35
|
+
getExtensionStatusPlacement,
|
|
36
|
+
type IconMode,
|
|
37
|
+
isExtensionStatusColorMode,
|
|
38
|
+
isExtensionStatusPlacement,
|
|
39
|
+
isSeparatorStyle,
|
|
40
|
+
isValidWorkingLineIntervalMs,
|
|
41
|
+
MAX_WORKING_LINE_INTERVAL_MS,
|
|
42
|
+
MIN_WORKING_LINE_INTERVAL_MS,
|
|
43
|
+
type MinimalistConfig,
|
|
44
|
+
type ModelLabelSource,
|
|
45
|
+
type PathDisplayConfig,
|
|
46
|
+
type PolishedCopyFriendlyEditorStyleConfig,
|
|
47
|
+
type PolishedEditorStyleConfig,
|
|
48
|
+
type PolishedTuiConfig,
|
|
49
|
+
type SelectorBordersComponentConfig,
|
|
50
|
+
type SeparatorStyle,
|
|
51
|
+
type UserMessageStyle,
|
|
52
|
+
type UserMessagesComponentConfig,
|
|
53
|
+
type WorkingLineComponentPatch,
|
|
54
|
+
type WorkingLineSpinner,
|
|
55
|
+
type WorkingLineTextAnimation,
|
|
56
|
+
} from "../app/config/shell.ts";
|
|
57
|
+
import { sanitizeExtensionStatusText } from "./extension-status.ts";
|
|
58
|
+
import { isIconMode } from "./icons.ts";
|
|
59
|
+
import type { SessionLifecycle } from "./session-lifecycle.ts";
|
|
60
|
+
import {
|
|
61
|
+
renderEditorSettingsPreview,
|
|
62
|
+
renderUserMessageSettingsPreview,
|
|
63
|
+
SETTINGS_PREVIEW_MAX_WIDTH,
|
|
64
|
+
} from "./settings-previews.ts";
|
|
65
|
+
import {
|
|
66
|
+
EDITOR_BORDER_STYLE,
|
|
67
|
+
renderChromeBorder,
|
|
68
|
+
safeThemeFg,
|
|
69
|
+
} from "./style.ts";
|
|
70
|
+
import {
|
|
71
|
+
buildWorkingLinePreviewFrames,
|
|
72
|
+
normalizeWorkingLineMessages,
|
|
73
|
+
remapWorkingLineTextTick,
|
|
74
|
+
type WorkingLineFrames,
|
|
75
|
+
} from "./working-line.ts";
|
|
76
|
+
|
|
77
|
+
const colorSourceValues: ColorSource[] = ["theme", "terminal"];
|
|
78
|
+
const extensionStatusPlacementValues: ExtensionStatusPlacement[] = [
|
|
79
|
+
"off",
|
|
80
|
+
"left",
|
|
81
|
+
"middle",
|
|
82
|
+
"right",
|
|
83
|
+
];
|
|
84
|
+
const extensionStatusColorModeValues: ExtensionStatusColorMode[] = [
|
|
85
|
+
"zentui",
|
|
86
|
+
"original",
|
|
87
|
+
];
|
|
88
|
+
const contextStyleValues: ContextStyle[] = ["text", "gauge", "text+gauge"];
|
|
89
|
+
const separatorStyleValues: SeparatorStyle[] = [
|
|
90
|
+
"pipe",
|
|
91
|
+
"dot",
|
|
92
|
+
"chevron",
|
|
93
|
+
"none",
|
|
94
|
+
];
|
|
95
|
+
const pathDisplayModeValues = ["basename", "full"];
|
|
96
|
+
const pathDepthValues = ["0", "1", "2", "3", "4", "5"];
|
|
97
|
+
const branchLengthPresetValues = ["full", "10", "20", "30", "40", "50"];
|
|
98
|
+
const iconModeValues: IconMode[] = ["auto", "nerd", "ascii"];
|
|
99
|
+
const modelLabelValues: ModelLabelSource[] = ["id", "name"];
|
|
100
|
+
const editorStyleLabels: Record<EditorStyle, string> = {
|
|
101
|
+
opencode: "Opencode",
|
|
102
|
+
"opencode-copy-friendly": "Opencode (copy-friendly)",
|
|
103
|
+
"accent-rail": "Accent Rail",
|
|
104
|
+
minimalist: "Minimalist",
|
|
105
|
+
};
|
|
106
|
+
const editorStyleValues = Object.values(editorStyleLabels);
|
|
107
|
+
const userMessageStyleLabels: Record<UserMessageStyle, string> = {
|
|
108
|
+
framed: "Framed",
|
|
109
|
+
"framed-copy-friendly": "Framed (copy-friendly)",
|
|
110
|
+
compact: "Compact",
|
|
111
|
+
labeled: "Labeled",
|
|
112
|
+
};
|
|
113
|
+
const userMessageStyleValues = Object.values(userMessageStyleLabels);
|
|
114
|
+
const footerStyleLabels: Record<FooterStyle, string> = {
|
|
115
|
+
native: "Native",
|
|
116
|
+
starship: "Starship",
|
|
117
|
+
hidden: "Hidden",
|
|
118
|
+
};
|
|
119
|
+
const footerStyleValues = Object.values(footerStyleLabels);
|
|
120
|
+
const completionMenuValues: CompletionMenuStyle[] = ["palette", "native"];
|
|
121
|
+
const accentRailSurfaceValues = ["filled", "transparent"];
|
|
122
|
+
const minimalistPathDisplayValues = ["compact", "project", "full"];
|
|
123
|
+
const minimalistContextFormatValues = ["percent", "percent-total"];
|
|
124
|
+
const editorBorderColorModeValues: EditorBorderColorMode[] = [
|
|
125
|
+
"static",
|
|
126
|
+
"adaptive",
|
|
127
|
+
];
|
|
128
|
+
const compactFooterMaxLineValues = ["1", "2", "3", "unlimited"];
|
|
129
|
+
const featureStateValues: FeatureState[] = ["enabled", "disabled"];
|
|
130
|
+
const workingLineSpinnerLabels: Record<WorkingLineSpinner, string> = {
|
|
131
|
+
braille: "Braille Orbit",
|
|
132
|
+
"star-bloom": "Star Bloom",
|
|
133
|
+
pinwheel: "ASCII Pinwheel",
|
|
134
|
+
"claude-inspired": "Claude-inspired",
|
|
135
|
+
pulse: "Pulse",
|
|
136
|
+
};
|
|
137
|
+
const workingLineSpinnerValues = Object.values(workingLineSpinnerLabels);
|
|
138
|
+
const workingLineSpinnerSpeedPresets = [
|
|
139
|
+
{ label: "Fast 60 ms", intervalMs: 60 },
|
|
140
|
+
{ label: "Normal 100 ms", intervalMs: 100 },
|
|
141
|
+
{ label: "Slow 160 ms", intervalMs: 160 },
|
|
142
|
+
] as const;
|
|
143
|
+
const workingLineTextSpeedPresets = [
|
|
144
|
+
{ label: "Fast 40 ms", intervalMs: 40 },
|
|
145
|
+
{ label: "Normal 60 ms", intervalMs: 60 },
|
|
146
|
+
{ label: "Slow 100 ms", intervalMs: 100 },
|
|
147
|
+
] as const;
|
|
148
|
+
const speedValues = (presets: readonly { label: string }[]) => [
|
|
149
|
+
...presets.map(({ label }) => label),
|
|
150
|
+
"Custom…",
|
|
151
|
+
];
|
|
152
|
+
const workingLineTextAnimationValues: WorkingLineTextAnimation[] = [
|
|
153
|
+
"classic",
|
|
154
|
+
"kitt",
|
|
155
|
+
"disabled",
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const settingsSections = [
|
|
159
|
+
"appearance",
|
|
160
|
+
"editor",
|
|
161
|
+
"userMessages",
|
|
162
|
+
"workingLine",
|
|
163
|
+
"footer",
|
|
164
|
+
"segments",
|
|
165
|
+
"git",
|
|
166
|
+
"extensions",
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
type FeatureState = "enabled" | "disabled";
|
|
170
|
+
type SettingsSection = (typeof settingsSections)[number];
|
|
171
|
+
type FooterSegmentSettingId = keyof FooterSegmentsConfig;
|
|
172
|
+
type EditorPatch = Partial<
|
|
173
|
+
Pick<
|
|
174
|
+
EditorComponentConfig,
|
|
175
|
+
| "enabled"
|
|
176
|
+
| "style"
|
|
177
|
+
| "colorSource"
|
|
178
|
+
| "borderColorMode"
|
|
179
|
+
| "modelLabel"
|
|
180
|
+
| "viewportIndicators"
|
|
181
|
+
>
|
|
182
|
+
>;
|
|
183
|
+
type UserMessagesPatch = Partial<
|
|
184
|
+
Pick<UserMessagesComponentConfig, "enabled" | "style" | "colorSource">
|
|
185
|
+
>;
|
|
186
|
+
type FooterPatch = Partial<
|
|
187
|
+
Pick<FooterComponentConfig, "style" | "colorSource" | "modelLabel">
|
|
188
|
+
>;
|
|
189
|
+
type ApplyResult = { applied: boolean; reason?: string };
|
|
190
|
+
type SettingsOutcome =
|
|
191
|
+
| "close"
|
|
192
|
+
| "edit-working-line-messages"
|
|
193
|
+
| "edit-working-line-spinner-speed"
|
|
194
|
+
| "edit-working-line-text-speed";
|
|
195
|
+
|
|
196
|
+
type SettingsCommandDeps = {
|
|
197
|
+
sessionLifecycle: SessionLifecycle;
|
|
198
|
+
getConfig: () => PolishedTuiConfig;
|
|
199
|
+
setEditorComponent: (
|
|
200
|
+
patch: EditorPatch,
|
|
201
|
+
ctx: ExtensionContext,
|
|
202
|
+
) => ApplyResult;
|
|
203
|
+
setPolished: (
|
|
204
|
+
patch: Partial<PolishedEditorStyleConfig>,
|
|
205
|
+
ctx: ExtensionContext,
|
|
206
|
+
) => void;
|
|
207
|
+
setPolishedCopyFriendly: (
|
|
208
|
+
patch: Partial<PolishedCopyFriendlyEditorStyleConfig>,
|
|
209
|
+
ctx: ExtensionContext,
|
|
210
|
+
) => void;
|
|
211
|
+
setAccentRail: (
|
|
212
|
+
patch: Partial<AccentRailEditorStyleConfig>,
|
|
213
|
+
ctx: ExtensionContext,
|
|
214
|
+
) => void;
|
|
215
|
+
setMinimalist: (
|
|
216
|
+
patch: Partial<MinimalistConfig>,
|
|
217
|
+
ctx: ExtensionContext,
|
|
218
|
+
) => void;
|
|
219
|
+
setUserMessagesComponent: (
|
|
220
|
+
patch: UserMessagesPatch,
|
|
221
|
+
ctx: ExtensionContext,
|
|
222
|
+
) => void;
|
|
223
|
+
setWorkingLineComponent: (
|
|
224
|
+
patch: WorkingLineComponentPatch,
|
|
225
|
+
ctx: ExtensionContext,
|
|
226
|
+
) => ApplyResult;
|
|
227
|
+
setSelectorBordersComponent: (
|
|
228
|
+
patch: Partial<SelectorBordersComponentConfig>,
|
|
229
|
+
ctx: ExtensionContext,
|
|
230
|
+
) => void;
|
|
231
|
+
setFooterComponent: (patch: FooterPatch, ctx: ExtensionContext) => void;
|
|
232
|
+
setFooterSegments: (
|
|
233
|
+
patch: Partial<FooterSegmentsConfig>,
|
|
234
|
+
ctx: ExtensionContext,
|
|
235
|
+
) => void;
|
|
236
|
+
setFooterFormat: (value: string, ctx: ExtensionContext) => void;
|
|
237
|
+
setResponsiveFooter: (
|
|
238
|
+
patch: Partial<
|
|
239
|
+
Pick<PolishedTuiConfig, "responsiveFooter" | "compactFooterMaxLines">
|
|
240
|
+
>,
|
|
241
|
+
ctx: ExtensionContext,
|
|
242
|
+
) => void;
|
|
243
|
+
setIconMode: (mode: IconMode) => void;
|
|
244
|
+
setContextStyle: (style: ContextStyle) => void;
|
|
245
|
+
setSeparator: (separator: SeparatorStyle) => void;
|
|
246
|
+
setPathDisplay: (patch: Partial<PathDisplayConfig>) => void;
|
|
247
|
+
setGitBranch: (patch: Partial<GitBranchConfig>) => void;
|
|
248
|
+
setGitCommit: (
|
|
249
|
+
patch: Partial<Pick<GitCommitConfig, "onlyDetached" | "showTag">>,
|
|
250
|
+
ctx: ExtensionContext,
|
|
251
|
+
) => void;
|
|
252
|
+
setGitMetrics: (
|
|
253
|
+
patch: Partial<GitMetricsConfig>,
|
|
254
|
+
ctx: ExtensionContext,
|
|
255
|
+
) => void;
|
|
256
|
+
getActiveExtensionStatuses: () => ReadonlyMap<string, string>;
|
|
257
|
+
setExtensionStatusDefaultPlacement: (
|
|
258
|
+
placement: ExtensionStatusPlacement,
|
|
259
|
+
) => void;
|
|
260
|
+
setExtensionStatusPlacement: (
|
|
261
|
+
key: string,
|
|
262
|
+
placement: ExtensionStatusPlacement,
|
|
263
|
+
) => void;
|
|
264
|
+
setExtensionStatusColorMode: (
|
|
265
|
+
key: string,
|
|
266
|
+
colorMode: ExtensionStatusColorMode,
|
|
267
|
+
) => void;
|
|
268
|
+
requestRender: () => void;
|
|
269
|
+
settingsListTheme?: SettingsListTheme;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const sectionLabels: Record<SettingsSection, string> = {
|
|
273
|
+
appearance: "Appearance",
|
|
274
|
+
editor: "Editor",
|
|
275
|
+
userMessages: "User messages",
|
|
276
|
+
workingLine: "Working line",
|
|
277
|
+
footer: "Footer",
|
|
278
|
+
segments: "Segments",
|
|
279
|
+
git: "Git",
|
|
280
|
+
extensions: "Extensions",
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
|
|
284
|
+
cwd: "Current directory",
|
|
285
|
+
sessionName: "Session name",
|
|
286
|
+
gitBranch: "Git branch",
|
|
287
|
+
gitStatus: "Git status",
|
|
288
|
+
gitCounts: "Git counts",
|
|
289
|
+
sessionDuration: "Session duration",
|
|
290
|
+
username: "Username@host",
|
|
291
|
+
time: "Current time",
|
|
292
|
+
os: "OS icon",
|
|
293
|
+
runtime: "Runtime",
|
|
294
|
+
modelInfo: "Model info",
|
|
295
|
+
context: "Context usage",
|
|
296
|
+
tokens: "Token counts",
|
|
297
|
+
cost: "Session cost",
|
|
298
|
+
packageVersion: "Package version",
|
|
299
|
+
gitCommit: "Git commit",
|
|
300
|
+
gitMetrics: "Git line metrics",
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const footerSegmentSettingDescriptions: Record<FooterSegmentSettingId, string> =
|
|
304
|
+
{
|
|
305
|
+
cwd: "Show or hide the current working directory segment on the left.",
|
|
306
|
+
sessionName: "Show or hide the current Pi session name on the left.",
|
|
307
|
+
gitBranch: "Show or hide the git branch name on the left.",
|
|
308
|
+
gitStatus: "Show or hide git status icons and ahead/behind markers.",
|
|
309
|
+
gitCounts: "Show numeric ahead/behind and stash counts.",
|
|
310
|
+
sessionDuration: "Show session running time on the left.",
|
|
311
|
+
username: "Show user@hostname on the left.",
|
|
312
|
+
time: "Show the current time (HH:MM) on the right.",
|
|
313
|
+
os: "Show an operating-system icon on the left.",
|
|
314
|
+
runtime: "Show or hide the detected runtime/language segment.",
|
|
315
|
+
modelInfo: "Show the selected model and non-duplicate provider.",
|
|
316
|
+
context: "Show or hide context usage on the right.",
|
|
317
|
+
tokens: "Show or hide input/output token counts on the right.",
|
|
318
|
+
cost: "Show or hide session cost on the right.",
|
|
319
|
+
packageVersion: "Show the project manifest version.",
|
|
320
|
+
gitCommit: "Show the current commit hash and optional exact-match tag.",
|
|
321
|
+
gitMetrics: "Show aggregate added/deleted line counts.",
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const directCommandSuggestions = [
|
|
325
|
+
"editor enable",
|
|
326
|
+
"editor disable",
|
|
327
|
+
"editor toggle",
|
|
328
|
+
"messages enable",
|
|
329
|
+
"messages disable",
|
|
330
|
+
"messages toggle",
|
|
331
|
+
"statusline enable",
|
|
332
|
+
"statusline disable",
|
|
333
|
+
"statusline toggle",
|
|
334
|
+
"viewport-indicators enable",
|
|
335
|
+
"viewport-indicators disable",
|
|
336
|
+
"viewport-indicators toggle",
|
|
337
|
+
"messages",
|
|
338
|
+
"user-messages",
|
|
339
|
+
"working-line",
|
|
340
|
+
"format clear",
|
|
341
|
+
"format $cwd on $git_branch $fill $context",
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
const thirdPartyStatusSettingPrefix = "thirdPartyStatus:";
|
|
345
|
+
const footerSegmentSettingPrefix = "footerSegment:";
|
|
346
|
+
type ThirdPartyStatusSettingKind = "placement" | "colorMode";
|
|
347
|
+
|
|
348
|
+
function featureValue(enabled: boolean): FeatureState {
|
|
349
|
+
return enabled ? "enabled" : "disabled";
|
|
350
|
+
}
|
|
351
|
+
function editorStyleLabel(style: EditorStyle): string {
|
|
352
|
+
return editorStyleLabels[style];
|
|
353
|
+
}
|
|
354
|
+
function editorStyleId(label: string): EditorStyle | undefined {
|
|
355
|
+
return (
|
|
356
|
+
Object.entries(editorStyleLabels) as Array<[EditorStyle, string]>
|
|
357
|
+
).find(([, value]) => value === label)?.[0];
|
|
358
|
+
}
|
|
359
|
+
function footerStyleLabel(style: FooterStyle): string {
|
|
360
|
+
return footerStyleLabels[style];
|
|
361
|
+
}
|
|
362
|
+
function footerStyleId(label: string): FooterStyle | undefined {
|
|
363
|
+
return (
|
|
364
|
+
Object.entries(footerStyleLabels) as Array<[FooterStyle, string]>
|
|
365
|
+
).find(([, value]) => value === label)?.[0];
|
|
366
|
+
}
|
|
367
|
+
function userMessageStyleLabel(style: UserMessageStyle): string {
|
|
368
|
+
return userMessageStyleLabels[style];
|
|
369
|
+
}
|
|
370
|
+
function userMessageStyleId(label: string): UserMessageStyle | undefined {
|
|
371
|
+
return (
|
|
372
|
+
Object.entries(userMessageStyleLabels) as Array<[UserMessageStyle, string]>
|
|
373
|
+
).find(([, value]) => value === label)?.[0];
|
|
374
|
+
}
|
|
375
|
+
function workingLineSpinnerId(label: string): WorkingLineSpinner | undefined {
|
|
376
|
+
return (
|
|
377
|
+
Object.entries(workingLineSpinnerLabels) as Array<
|
|
378
|
+
[WorkingLineSpinner, string]
|
|
379
|
+
>
|
|
380
|
+
).find(([, value]) => value === label)?.[0];
|
|
381
|
+
}
|
|
382
|
+
function workingLineSpeedLabel(
|
|
383
|
+
intervalMs: number,
|
|
384
|
+
presets: readonly { label: string; intervalMs: number }[],
|
|
385
|
+
): string {
|
|
386
|
+
return (
|
|
387
|
+
presets.find((preset) => preset.intervalMs === intervalMs)?.label ??
|
|
388
|
+
`Custom ${intervalMs} ms`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function isFeatureState(value: string): value is FeatureState {
|
|
392
|
+
return value === "enabled" || value === "disabled";
|
|
393
|
+
}
|
|
394
|
+
function isColorSource(value: string): value is ColorSource {
|
|
395
|
+
return value === "theme" || value === "terminal";
|
|
396
|
+
}
|
|
397
|
+
function parseAction(
|
|
398
|
+
words: string[],
|
|
399
|
+
): "enable" | "disable" | "toggle" | undefined {
|
|
400
|
+
if (words.includes("toggle")) return "toggle";
|
|
401
|
+
if (words.some((word) => ["enable", "enabled", "on"].includes(word)))
|
|
402
|
+
return "enable";
|
|
403
|
+
if (words.some((word) => ["disable", "disabled", "off"].includes(word)))
|
|
404
|
+
return "disable";
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
function actionValue(
|
|
408
|
+
action: "enable" | "disable" | "toggle",
|
|
409
|
+
current: boolean,
|
|
410
|
+
): boolean {
|
|
411
|
+
return action === "toggle" ? !current : action === "enable";
|
|
412
|
+
}
|
|
413
|
+
function normalizedWords(args: string): string[] {
|
|
414
|
+
return args
|
|
415
|
+
.trim()
|
|
416
|
+
.toLowerCase()
|
|
417
|
+
.replaceAll(/[_-]+/g, " ")
|
|
418
|
+
.split(/\s+/g)
|
|
419
|
+
.filter(Boolean);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
type DirectOperation = {
|
|
423
|
+
kind: "editor" | "messages" | "footer" | "viewport";
|
|
424
|
+
enabled: boolean;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
function parseDirectOperation(
|
|
428
|
+
args: string,
|
|
429
|
+
config: PolishedTuiConfig,
|
|
430
|
+
): DirectOperation | undefined {
|
|
431
|
+
const words = normalizedWords(args);
|
|
432
|
+
const action = parseAction(words);
|
|
433
|
+
if (!action) return undefined;
|
|
434
|
+
const actionWords = new Set([
|
|
435
|
+
"enable",
|
|
436
|
+
"enabled",
|
|
437
|
+
"on",
|
|
438
|
+
"disable",
|
|
439
|
+
"disabled",
|
|
440
|
+
"off",
|
|
441
|
+
"toggle",
|
|
442
|
+
]);
|
|
443
|
+
const target = words.filter((word) => !actionWords.has(word)).join(" ");
|
|
444
|
+
if (target === "viewportindicators" || target === "viewport indicators") {
|
|
445
|
+
return {
|
|
446
|
+
kind: "viewport",
|
|
447
|
+
enabled: actionValue(action, config.components.editor.viewportIndicators),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (target === "messages" || target === "user messages") {
|
|
451
|
+
return {
|
|
452
|
+
kind: "messages",
|
|
453
|
+
enabled: actionValue(action, config.components.userMessages.enabled),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (target === "editor") {
|
|
457
|
+
return {
|
|
458
|
+
kind: "editor",
|
|
459
|
+
enabled: actionValue(action, config.components.editor.enabled),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (["footer", "statusline", "status", "status line"].includes(target)) {
|
|
463
|
+
return {
|
|
464
|
+
kind: "footer",
|
|
465
|
+
enabled: actionValue(
|
|
466
|
+
action,
|
|
467
|
+
config.components.footer.style === "starship",
|
|
468
|
+
),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function parseFormatCommand(
|
|
475
|
+
args: string,
|
|
476
|
+
): { value: string | undefined } | undefined {
|
|
477
|
+
const trimmed = args.trim();
|
|
478
|
+
if (!trimmed.toLowerCase().startsWith("format")) return undefined;
|
|
479
|
+
const rest = trimmed.slice("format".length).trim();
|
|
480
|
+
if (!rest || rest.toLowerCase() === "clear") return { value: undefined };
|
|
481
|
+
return {
|
|
482
|
+
value:
|
|
483
|
+
rest.startsWith('"') && rest.endsWith('"') && rest.length >= 2
|
|
484
|
+
? rest.slice(1, -1)
|
|
485
|
+
: rest,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function directSection(args: string): SettingsSection | undefined {
|
|
490
|
+
const normalized = args.trim().toLowerCase().replaceAll("_", "-");
|
|
491
|
+
if (
|
|
492
|
+
normalized === "messages" ||
|
|
493
|
+
normalized === "user-messages" ||
|
|
494
|
+
normalized === "user messages"
|
|
495
|
+
) {
|
|
496
|
+
return "userMessages";
|
|
497
|
+
}
|
|
498
|
+
if (normalized === "working-line" || normalized === "working line")
|
|
499
|
+
return "workingLine";
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function argumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
504
|
+
const normalized = prefix.trimStart().toLowerCase();
|
|
505
|
+
const matches = directCommandSuggestions
|
|
506
|
+
.map((value) => ({ value, label: value }))
|
|
507
|
+
.filter((item) => item.value.startsWith(normalized));
|
|
508
|
+
return matches.length ? matches : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function usageText(): string {
|
|
512
|
+
return "Usage: /zentui [editor|messages|statusline|viewport-indicators] [enable|disable|toggle], /zentui [messages|user-messages|working-line], or /zentui format <template>";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function buildAppearanceItems(config: PolishedTuiConfig): SettingItem[] {
|
|
516
|
+
const component = config.components.selectorBorders;
|
|
517
|
+
return [
|
|
518
|
+
{
|
|
519
|
+
id: "selectorBordersEnabled",
|
|
520
|
+
label: "Selector borders",
|
|
521
|
+
description: "Enable or disable Zentui borders around Pi selectors.",
|
|
522
|
+
currentValue: featureValue(component.enabled),
|
|
523
|
+
values: featureStateValues,
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: "selectorBordersStyle",
|
|
527
|
+
label: "Selector border style",
|
|
528
|
+
description: "Choose the selector-border style.",
|
|
529
|
+
currentValue: component.style,
|
|
530
|
+
values: ["zentui"],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
id: "selectorBordersColorSource",
|
|
534
|
+
label: "Selector border colors",
|
|
535
|
+
description: "Use Pi theme colors or terminal palette styles.",
|
|
536
|
+
currentValue: component.colorSource,
|
|
537
|
+
values: colorSourceValues,
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: "iconMode",
|
|
541
|
+
label: "Icon mode",
|
|
542
|
+
description:
|
|
543
|
+
"auto/nerd use Nerd Font glyphs; ascii uses plain fallbacks.",
|
|
544
|
+
currentValue: config.icons.mode,
|
|
545
|
+
values: iconModeValues,
|
|
546
|
+
},
|
|
547
|
+
];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function buildEditorItems(config: PolishedTuiConfig): SettingItem[] {
|
|
551
|
+
const editor = config.components.editor;
|
|
552
|
+
return [
|
|
553
|
+
{
|
|
554
|
+
id: "editorEnabled",
|
|
555
|
+
label: "Editor",
|
|
556
|
+
description: "Enable or disable Zentui's custom editor.",
|
|
557
|
+
currentValue: featureValue(editor.enabled),
|
|
558
|
+
values: featureStateValues,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: "editorStyle",
|
|
562
|
+
label: "Editor style",
|
|
563
|
+
description: "Use Opencode, Accent Rail, or a compact Minimalist frame.",
|
|
564
|
+
currentValue: editorStyleLabel(editor.style),
|
|
565
|
+
values: editorStyleValues,
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "editorColorSource",
|
|
569
|
+
label: "Editor colors",
|
|
570
|
+
description: "Use Pi theme colors or terminal palette styles.",
|
|
571
|
+
currentValue: editor.colorSource,
|
|
572
|
+
values: colorSourceValues,
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: "editorModelLabel",
|
|
576
|
+
label: "Editor model label",
|
|
577
|
+
description: "Show the model id or display name in the editor frame.",
|
|
578
|
+
currentValue: editor.modelLabel,
|
|
579
|
+
values: modelLabelValues,
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
id: "editorBorderColorMode",
|
|
583
|
+
label: "Editor border color",
|
|
584
|
+
description: "Keep configured color or follow Pi's shell/thinking color.",
|
|
585
|
+
currentValue: editor.borderColorMode,
|
|
586
|
+
values: editorBorderColorModeValues,
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
id: "editorViewportIndicators",
|
|
590
|
+
label: "Editor viewport indicators",
|
|
591
|
+
description: "Show Pi's native wrapped-row counts in editor borders.",
|
|
592
|
+
currentValue: featureValue(editor.viewportIndicators),
|
|
593
|
+
values: featureStateValues,
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function buildPolishedEditorStyleItems(
|
|
599
|
+
config: PolishedTuiConfig,
|
|
600
|
+
): SettingItem[] {
|
|
601
|
+
const editor = config.components.editor;
|
|
602
|
+
const style =
|
|
603
|
+
editor.style === "opencode-copy-friendly"
|
|
604
|
+
? editor.styles["opencode-copy-friendly"]
|
|
605
|
+
: editor.styles.opencode;
|
|
606
|
+
return [
|
|
607
|
+
{
|
|
608
|
+
id: "opencodeCompletionMenu",
|
|
609
|
+
label: "Completion menu",
|
|
610
|
+
description:
|
|
611
|
+
"Use Pi's native list or the full-width Opencode palette shell.",
|
|
612
|
+
currentValue: style.completionMenu,
|
|
613
|
+
values: completionMenuValues,
|
|
614
|
+
},
|
|
615
|
+
];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function buildAccentRailEditorStyleItems(
|
|
619
|
+
config: PolishedTuiConfig,
|
|
620
|
+
): SettingItem[] {
|
|
621
|
+
const accentRail = config.components.editor.styles["accent-rail"];
|
|
622
|
+
return [
|
|
623
|
+
{
|
|
624
|
+
id: "accentRailSurface",
|
|
625
|
+
label: "Accent Rail surface",
|
|
626
|
+
description:
|
|
627
|
+
"Fill input and autocomplete surfaces or keep them transparent.",
|
|
628
|
+
currentValue: accentRail.transparent ? "transparent" : "filled",
|
|
629
|
+
values: accentRailSurfaceValues,
|
|
630
|
+
},
|
|
631
|
+
];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function buildMinimalistEditorStyleItems(
|
|
635
|
+
config: PolishedTuiConfig,
|
|
636
|
+
): SettingItem[] {
|
|
637
|
+
const minimalist = config.components.editor.styles.minimalist;
|
|
638
|
+
return [
|
|
639
|
+
{
|
|
640
|
+
id: "minimalistPathDisplay",
|
|
641
|
+
label: "Path",
|
|
642
|
+
description: "Show compact, project-relative, or full path.",
|
|
643
|
+
currentValue: minimalist.pathDisplay,
|
|
644
|
+
values: minimalistPathDisplayValues,
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
id: "minimalistContextFormat",
|
|
648
|
+
label: "Context text",
|
|
649
|
+
description: "Show percent alone or with total context.",
|
|
650
|
+
currentValue: minimalist.contextFormat,
|
|
651
|
+
values: minimalistContextFormatValues,
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: "minimalistContextGauge",
|
|
655
|
+
label: "Context gauge",
|
|
656
|
+
description: "Add a compact context gauge.",
|
|
657
|
+
currentValue: featureValue(minimalist.contextGauge),
|
|
658
|
+
values: featureStateValues,
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
id: "minimalistShowSessionName",
|
|
662
|
+
label: "Session name",
|
|
663
|
+
description: "Show the explicit Pi session name.",
|
|
664
|
+
currentValue: featureValue(minimalist.showSessionName),
|
|
665
|
+
values: featureStateValues,
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
id: "minimalistShowTimer",
|
|
669
|
+
label: "Timer",
|
|
670
|
+
description: "Show current or completed turn duration.",
|
|
671
|
+
currentValue: featureValue(minimalist.showTimer),
|
|
672
|
+
values: featureStateValues,
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
id: "minimalistShowCost",
|
|
676
|
+
label: "Cost",
|
|
677
|
+
description: "Show session cost in the top border.",
|
|
678
|
+
currentValue: featureValue(minimalist.showCost),
|
|
679
|
+
values: featureStateValues,
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
id: "minimalistShowGit",
|
|
683
|
+
label: "Git",
|
|
684
|
+
description: "Show branch and working-tree state.",
|
|
685
|
+
currentValue: featureValue(minimalist.showGit),
|
|
686
|
+
values: featureStateValues,
|
|
687
|
+
},
|
|
688
|
+
];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function buildUserMessagesItems(config: PolishedTuiConfig): SettingItem[] {
|
|
692
|
+
const messages = config.components.userMessages;
|
|
693
|
+
return [
|
|
694
|
+
{
|
|
695
|
+
id: "userMessagesEnabled",
|
|
696
|
+
label: "User messages",
|
|
697
|
+
description: "Enable or disable previous user-message styling.",
|
|
698
|
+
currentValue: featureValue(messages.enabled),
|
|
699
|
+
values: featureStateValues,
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
id: "userMessagesStyle",
|
|
703
|
+
label: "Message style",
|
|
704
|
+
description: "Choose the previous-message style.",
|
|
705
|
+
currentValue: userMessageStyleLabel(messages.style),
|
|
706
|
+
values: userMessageStyleValues,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: "userMessagesColorSource",
|
|
710
|
+
label: "Message colors",
|
|
711
|
+
description: "Use Pi theme colors or terminal palette styles.",
|
|
712
|
+
currentValue: messages.colorSource,
|
|
713
|
+
values: colorSourceValues,
|
|
714
|
+
},
|
|
715
|
+
];
|
|
716
|
+
}
|
|
717
|
+
function buildWorkingLineItems(config: PolishedTuiConfig): SettingItem[] {
|
|
718
|
+
const workingLine = config.components.workingLine;
|
|
719
|
+
const staticText = workingLine.textAnimation === "disabled";
|
|
720
|
+
const staticNote = staticText
|
|
721
|
+
? " Inactive in Static mode; saved for animated modes."
|
|
722
|
+
: "";
|
|
723
|
+
return [
|
|
724
|
+
{
|
|
725
|
+
id: "workingLineEnabled",
|
|
726
|
+
label: "Enabled",
|
|
727
|
+
description: "Own and stylize Pi's complete working row.",
|
|
728
|
+
currentValue: featureValue(workingLine.enabled),
|
|
729
|
+
values: featureStateValues,
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
id: "workingLineTurnSummary",
|
|
733
|
+
label: "Turn summary",
|
|
734
|
+
description: workingLine.enabled
|
|
735
|
+
? "Append `Turn took …` after each fully settled interaction."
|
|
736
|
+
: "Append `Turn took …` after each fully settled interaction; inactive while Working line disabled.",
|
|
737
|
+
currentValue: featureValue(workingLine.turnSummary),
|
|
738
|
+
values: featureStateValues,
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
id: "workingLineSpinner",
|
|
742
|
+
label: "Spinner",
|
|
743
|
+
description:
|
|
744
|
+
"Choose the fixed-width spinner preset; glyph motion is always active.",
|
|
745
|
+
currentValue: workingLineSpinnerLabels[workingLine.spinner],
|
|
746
|
+
values: workingLineSpinnerValues,
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
id: "workingLineSpinnerSpeed",
|
|
750
|
+
label: "Spinner speed",
|
|
751
|
+
description: `Set glyph cadence (${MIN_WORKING_LINE_INTERVAL_MS}–${MAX_WORKING_LINE_INTERVAL_MS} ms).`,
|
|
752
|
+
currentValue: workingLineSpeedLabel(
|
|
753
|
+
workingLine.spinnerIntervalMs,
|
|
754
|
+
workingLineSpinnerSpeedPresets,
|
|
755
|
+
),
|
|
756
|
+
values: speedValues(workingLineSpinnerSpeedPresets),
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
id: "workingLineAnimateSpinnerColor",
|
|
760
|
+
label: "Animate spinner color",
|
|
761
|
+
description: `Include spinner cells and separator in Classic/KITT color motion; glyph motion remains active.${staticNote}`,
|
|
762
|
+
currentValue: featureValue(workingLine.animateSpinnerColor),
|
|
763
|
+
values: featureStateValues,
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
id: "workingLineTextAnimation",
|
|
767
|
+
label: "Text animation",
|
|
768
|
+
description: "Animate the owned row or keep it uniformly static.",
|
|
769
|
+
currentValue: workingLine.textAnimation,
|
|
770
|
+
values: workingLineTextAnimationValues,
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
id: "workingLineTextSpeed",
|
|
774
|
+
label: "Text motion speed",
|
|
775
|
+
description: `Set Classic/KITT color cadence (${MIN_WORKING_LINE_INTERVAL_MS}–${MAX_WORKING_LINE_INTERVAL_MS} ms).${staticNote}`,
|
|
776
|
+
currentValue: workingLineSpeedLabel(
|
|
777
|
+
workingLine.textIntervalMs,
|
|
778
|
+
workingLineTextSpeedPresets,
|
|
779
|
+
),
|
|
780
|
+
values: speedValues(workingLineTextSpeedPresets),
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
id: "workingLineColorSource",
|
|
784
|
+
label: "Color source",
|
|
785
|
+
description:
|
|
786
|
+
"Use Pi theme colors or independent terminal palette styles.",
|
|
787
|
+
currentValue: workingLine.colorSource,
|
|
788
|
+
values: colorSourceValues,
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
id: "workingLineCustomMessages",
|
|
792
|
+
label: "Custom messages",
|
|
793
|
+
description:
|
|
794
|
+
"Select from the editable list once per turn; off uses styled Working… without RNG.",
|
|
795
|
+
currentValue: featureValue(workingLine.messages.custom),
|
|
796
|
+
values: featureStateValues,
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
id: "workingLineTool",
|
|
800
|
+
label: "Tool",
|
|
801
|
+
description: "Show the latest active tool.",
|
|
802
|
+
currentValue: featureValue(workingLine.segments.tool),
|
|
803
|
+
values: featureStateValues,
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
id: "workingLineElapsed",
|
|
807
|
+
label: "Elapsed",
|
|
808
|
+
description: "Show whole-interaction elapsed time.",
|
|
809
|
+
currentValue: featureValue(workingLine.segments.elapsed),
|
|
810
|
+
values: featureStateValues,
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
id: "workingLineThought",
|
|
814
|
+
label: "Thinking",
|
|
815
|
+
description:
|
|
816
|
+
"Show cumulative wall-clock thinking time and active updates.",
|
|
817
|
+
currentValue: featureValue(workingLine.segments.thought),
|
|
818
|
+
values: featureStateValues,
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
id: "workingLineTokens",
|
|
822
|
+
label: "Tokens",
|
|
823
|
+
description:
|
|
824
|
+
"Show whole-interaction tokens as ↑input ↓output; live output may be estimated until final usage reconciles.",
|
|
825
|
+
currentValue: featureValue(workingLine.segments.tokens),
|
|
826
|
+
values: featureStateValues,
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
id: "workingLineMessageList",
|
|
830
|
+
label: "Message list",
|
|
831
|
+
description:
|
|
832
|
+
"Edit one message per line; line order is preserved even while custom messages are off.",
|
|
833
|
+
currentValue: "Edit…",
|
|
834
|
+
values: ["Edit…"],
|
|
835
|
+
},
|
|
836
|
+
];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function buildFooterItems(config: PolishedTuiConfig): SettingItem[] {
|
|
840
|
+
const footer = config.components.footer;
|
|
841
|
+
const items: SettingItem[] = [
|
|
842
|
+
{
|
|
843
|
+
id: "footerStyle",
|
|
844
|
+
label: "Footer style",
|
|
845
|
+
description:
|
|
846
|
+
"Use Pi's native footer, Zentui's Starship footer, or no footer rows.",
|
|
847
|
+
currentValue: footerStyleLabel(footer.style),
|
|
848
|
+
values: footerStyleValues,
|
|
849
|
+
},
|
|
850
|
+
];
|
|
851
|
+
if (footer.style === "starship") {
|
|
852
|
+
items.push(
|
|
853
|
+
{
|
|
854
|
+
id: "footerColorSource",
|
|
855
|
+
label: "Footer colors",
|
|
856
|
+
description: "Use Pi theme colors or terminal palette styles.",
|
|
857
|
+
currentValue: footer.colorSource,
|
|
858
|
+
values: colorSourceValues,
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
id: "footerModelLabel",
|
|
862
|
+
label: "Footer model label",
|
|
863
|
+
description: "Show the model id or display name in the footer.",
|
|
864
|
+
currentValue: footer.modelLabel,
|
|
865
|
+
values: modelLabelValues,
|
|
866
|
+
},
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return items;
|
|
870
|
+
}
|
|
871
|
+
function buildStarshipFooterStyleItems(
|
|
872
|
+
config: PolishedTuiConfig,
|
|
873
|
+
): SettingItem[] {
|
|
874
|
+
const footer = config.components.footer.styles.starship;
|
|
875
|
+
return [
|
|
876
|
+
{
|
|
877
|
+
id: "responsiveFooter",
|
|
878
|
+
label: "Responsive footer",
|
|
879
|
+
description: "Use the compact template when space is tight.",
|
|
880
|
+
currentValue: featureValue(footer.responsive),
|
|
881
|
+
values: featureStateValues,
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
id: "compactFooterMaxLines",
|
|
885
|
+
label: "Compact footer rows",
|
|
886
|
+
description: "Maximum compact rows before cropping.",
|
|
887
|
+
currentValue: String(footer.compactMaxLines),
|
|
888
|
+
values: compactFooterMaxLineValues,
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
id: "contextStyle",
|
|
892
|
+
label: "Context style",
|
|
893
|
+
description: "Render context as text, gauge, or both.",
|
|
894
|
+
currentValue: footer.contextStyle,
|
|
895
|
+
values: contextStyleValues,
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
id: "separator",
|
|
899
|
+
label: "Separator",
|
|
900
|
+
description: "Choose the separator between default footer segments.",
|
|
901
|
+
currentValue: footer.separator,
|
|
902
|
+
values: separatorStyleValues,
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
id: "pathDisplay",
|
|
906
|
+
label: "Path display",
|
|
907
|
+
description: "Show cwd as basename or full path.",
|
|
908
|
+
currentValue: footer.pathDisplay.mode,
|
|
909
|
+
values: pathDisplayModeValues,
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
id: "pathDepth",
|
|
913
|
+
label: "Path depth",
|
|
914
|
+
description: "Trailing directories in full mode (0 = all).",
|
|
915
|
+
currentValue: String(footer.pathDisplay.depth),
|
|
916
|
+
values: pathDepthValues,
|
|
917
|
+
},
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const nonGitSegmentKeys: FooterSegmentSettingId[] = [
|
|
922
|
+
"cwd",
|
|
923
|
+
"sessionName",
|
|
924
|
+
"runtime",
|
|
925
|
+
"modelInfo",
|
|
926
|
+
"context",
|
|
927
|
+
"tokens",
|
|
928
|
+
"cost",
|
|
929
|
+
"sessionDuration",
|
|
930
|
+
"username",
|
|
931
|
+
"time",
|
|
932
|
+
"os",
|
|
933
|
+
"packageVersion",
|
|
934
|
+
];
|
|
935
|
+
function footerSegmentSettingId(key: FooterSegmentSettingId): string {
|
|
936
|
+
return `${footerSegmentSettingPrefix}${key}`;
|
|
937
|
+
}
|
|
938
|
+
function isFooterSegmentSettingId(
|
|
939
|
+
value: string,
|
|
940
|
+
): value is FooterSegmentSettingId {
|
|
941
|
+
return value in footerSegmentSettingLabels;
|
|
942
|
+
}
|
|
943
|
+
function footerSegmentSettingFromId(
|
|
944
|
+
id: string,
|
|
945
|
+
): FooterSegmentSettingId | undefined {
|
|
946
|
+
if (!id.startsWith(footerSegmentSettingPrefix)) return undefined;
|
|
947
|
+
const key = id.slice(footerSegmentSettingPrefix.length);
|
|
948
|
+
return isFooterSegmentSettingId(key) ? key : undefined;
|
|
949
|
+
}
|
|
950
|
+
function buildSegmentsItems(config: PolishedTuiConfig): SettingItem[] {
|
|
951
|
+
const segments = config.components.footer.styles.starship.segments;
|
|
952
|
+
return nonGitSegmentKeys.map((key) => ({
|
|
953
|
+
id: footerSegmentSettingId(key),
|
|
954
|
+
label: footerSegmentSettingLabels[key],
|
|
955
|
+
description: footerSegmentSettingDescriptions[key],
|
|
956
|
+
currentValue: featureValue(segments[key]),
|
|
957
|
+
values: featureStateValues,
|
|
958
|
+
}));
|
|
959
|
+
}
|
|
960
|
+
function branchLengthValues(maxLength: GitBranchMaxLength): string[] {
|
|
961
|
+
const current = String(maxLength);
|
|
962
|
+
return branchLengthPresetValues.includes(current as never)
|
|
963
|
+
? [...branchLengthPresetValues]
|
|
964
|
+
: [current, ...branchLengthPresetValues];
|
|
965
|
+
}
|
|
966
|
+
function buildGitItems(config: PolishedTuiConfig): SettingItem[] {
|
|
967
|
+
const starship = config.components.footer.styles.starship;
|
|
968
|
+
const segment = (key: FooterSegmentSettingId): SettingItem => ({
|
|
969
|
+
id: footerSegmentSettingId(key),
|
|
970
|
+
label: footerSegmentSettingLabels[key],
|
|
971
|
+
description: footerSegmentSettingDescriptions[key],
|
|
972
|
+
currentValue: featureValue(starship.segments[key]),
|
|
973
|
+
values: featureStateValues,
|
|
974
|
+
});
|
|
975
|
+
return [
|
|
976
|
+
segment("gitBranch"),
|
|
977
|
+
{
|
|
978
|
+
id: "branchLength",
|
|
979
|
+
label: "Branch length",
|
|
980
|
+
description: "Full branch name or a visible-width limit.",
|
|
981
|
+
currentValue: String(starship.gitBranch.maxLength),
|
|
982
|
+
values: branchLengthValues(starship.gitBranch.maxLength),
|
|
983
|
+
},
|
|
984
|
+
segment("gitStatus"),
|
|
985
|
+
segment("gitCounts"),
|
|
986
|
+
segment("gitCommit"),
|
|
987
|
+
{
|
|
988
|
+
id: "gitCommitOnlyDetached",
|
|
989
|
+
label: "Commit only on detached HEAD",
|
|
990
|
+
description: "Only show commit when HEAD is detached.",
|
|
991
|
+
currentValue: featureValue(starship.gitCommit.onlyDetached),
|
|
992
|
+
values: featureStateValues,
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
id: "gitCommitShowTag",
|
|
996
|
+
label: "Show exact-match tag",
|
|
997
|
+
description: "Append an exact-match tag.",
|
|
998
|
+
currentValue: featureValue(starship.gitCommit.showTag),
|
|
999
|
+
values: featureStateValues,
|
|
1000
|
+
},
|
|
1001
|
+
segment("gitMetrics"),
|
|
1002
|
+
{
|
|
1003
|
+
id: "gitMetricsOnlyNonzero",
|
|
1004
|
+
label: "Hide zero metrics",
|
|
1005
|
+
description: "Hide zero added/deleted values.",
|
|
1006
|
+
currentValue: featureValue(starship.gitMetrics.onlyNonzero),
|
|
1007
|
+
values: featureStateValues,
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
id: "gitMetricsIgnoreSubmodules",
|
|
1011
|
+
label: "Ignore submodules",
|
|
1012
|
+
description: "Exclude submodule changes.",
|
|
1013
|
+
currentValue: featureValue(starship.gitMetrics.ignoreSubmodules),
|
|
1014
|
+
values: featureStateValues,
|
|
1015
|
+
},
|
|
1016
|
+
];
|
|
1017
|
+
}
|
|
1018
|
+
function thirdPartyStatusSettingId(
|
|
1019
|
+
key: string,
|
|
1020
|
+
kind: ThirdPartyStatusSettingKind,
|
|
1021
|
+
): string {
|
|
1022
|
+
return `${thirdPartyStatusSettingPrefix}${kind}:${key}`;
|
|
1023
|
+
}
|
|
1024
|
+
function thirdPartyStatusSettingFromId(
|
|
1025
|
+
id: string,
|
|
1026
|
+
): { kind: ThirdPartyStatusSettingKind; key: string } | undefined {
|
|
1027
|
+
if (!id.startsWith(thirdPartyStatusSettingPrefix)) return undefined;
|
|
1028
|
+
const [kind, ...key] = id
|
|
1029
|
+
.slice(thirdPartyStatusSettingPrefix.length)
|
|
1030
|
+
.split(":");
|
|
1031
|
+
return kind === "placement" || kind === "colorMode"
|
|
1032
|
+
? { kind, key: key.join(":") }
|
|
1033
|
+
: undefined;
|
|
1034
|
+
}
|
|
1035
|
+
function buildExtensionsItems(
|
|
1036
|
+
config: PolishedTuiConfig,
|
|
1037
|
+
active: ReadonlyMap<string, string>,
|
|
1038
|
+
): SettingItem[] {
|
|
1039
|
+
const defaultItem: SettingItem = {
|
|
1040
|
+
id: "extensionStatusDefaultPlacement",
|
|
1041
|
+
label: "Default placement",
|
|
1042
|
+
description: "Placement for active statuses without an override.",
|
|
1043
|
+
currentValue:
|
|
1044
|
+
config.components.footer.styles.starship.extensionStatuses
|
|
1045
|
+
.defaultPlacement,
|
|
1046
|
+
values: extensionStatusPlacementValues,
|
|
1047
|
+
};
|
|
1048
|
+
const statuses = [...active.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
1049
|
+
if (!statuses.length)
|
|
1050
|
+
return [
|
|
1051
|
+
defaultItem,
|
|
1052
|
+
{
|
|
1053
|
+
id: "noThirdPartyStatuses",
|
|
1054
|
+
label: "No active statuses",
|
|
1055
|
+
description:
|
|
1056
|
+
"Only statuses currently published through ctx.ui.setStatus().",
|
|
1057
|
+
currentValue: "—",
|
|
1058
|
+
},
|
|
1059
|
+
];
|
|
1060
|
+
return [
|
|
1061
|
+
defaultItem,
|
|
1062
|
+
...statuses.flatMap(([key, value]) => {
|
|
1063
|
+
const sanitized = sanitizeExtensionStatusText(value);
|
|
1064
|
+
const description = sanitized
|
|
1065
|
+
? `Current status: ${sanitized}`
|
|
1066
|
+
: undefined;
|
|
1067
|
+
return [
|
|
1068
|
+
{
|
|
1069
|
+
id: thirdPartyStatusSettingId(key, "placement"),
|
|
1070
|
+
label: `${key} placement`,
|
|
1071
|
+
description,
|
|
1072
|
+
currentValue: getExtensionStatusPlacement(config, key),
|
|
1073
|
+
values: extensionStatusPlacementValues,
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
id: thirdPartyStatusSettingId(key, "colorMode"),
|
|
1077
|
+
label: `${key} color`,
|
|
1078
|
+
description,
|
|
1079
|
+
currentValue: getExtensionStatusColorMode(config, key),
|
|
1080
|
+
values: extensionStatusColorModeValues,
|
|
1081
|
+
},
|
|
1082
|
+
];
|
|
1083
|
+
}),
|
|
1084
|
+
];
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function buildSectionItems(
|
|
1088
|
+
section: SettingsSection,
|
|
1089
|
+
config: PolishedTuiConfig,
|
|
1090
|
+
active: ReadonlyMap<string, string>,
|
|
1091
|
+
): SettingItem[] {
|
|
1092
|
+
switch (section) {
|
|
1093
|
+
case "appearance":
|
|
1094
|
+
return buildAppearanceItems(config);
|
|
1095
|
+
case "editor":
|
|
1096
|
+
return [
|
|
1097
|
+
...buildEditorItems(config),
|
|
1098
|
+
...(config.components.editor.style === "opencode" ||
|
|
1099
|
+
config.components.editor.style === "opencode-copy-friendly"
|
|
1100
|
+
? buildPolishedEditorStyleItems(config)
|
|
1101
|
+
: []),
|
|
1102
|
+
...(config.components.editor.style === "accent-rail"
|
|
1103
|
+
? buildAccentRailEditorStyleItems(config)
|
|
1104
|
+
: []),
|
|
1105
|
+
...(config.components.editor.style === "minimalist"
|
|
1106
|
+
? buildMinimalistEditorStyleItems(config)
|
|
1107
|
+
: []),
|
|
1108
|
+
];
|
|
1109
|
+
case "userMessages":
|
|
1110
|
+
return buildUserMessagesItems(config);
|
|
1111
|
+
case "workingLine":
|
|
1112
|
+
return buildWorkingLineItems(config);
|
|
1113
|
+
case "footer":
|
|
1114
|
+
return [
|
|
1115
|
+
...buildFooterItems(config),
|
|
1116
|
+
...(config.components.footer.style === "starship"
|
|
1117
|
+
? buildStarshipFooterStyleItems(config)
|
|
1118
|
+
: []),
|
|
1119
|
+
];
|
|
1120
|
+
case "segments":
|
|
1121
|
+
return buildSegmentsItems(config);
|
|
1122
|
+
case "git":
|
|
1123
|
+
return buildGitItems(config);
|
|
1124
|
+
case "extensions":
|
|
1125
|
+
return buildExtensionsItems(config, active);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function nextSection(section: SettingsSection): SettingsSection {
|
|
1130
|
+
return (
|
|
1131
|
+
settingsSections[
|
|
1132
|
+
(settingsSections.indexOf(section) + 1) % settingsSections.length
|
|
1133
|
+
] ?? "appearance"
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
function previousSection(section: SettingsSection): SettingsSection {
|
|
1137
|
+
return (
|
|
1138
|
+
settingsSections[
|
|
1139
|
+
(settingsSections.indexOf(section) - 1 + settingsSections.length) %
|
|
1140
|
+
settingsSections.length
|
|
1141
|
+
] ?? "appearance"
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
function formatSectionTabs(
|
|
1145
|
+
active: SettingsSection,
|
|
1146
|
+
theme: ExtensionContext["ui"]["theme"],
|
|
1147
|
+
width: number,
|
|
1148
|
+
): string {
|
|
1149
|
+
const rendered = settingsSections.map((section) =>
|
|
1150
|
+
section === active
|
|
1151
|
+
? theme.bold(sectionLabels[section])
|
|
1152
|
+
: safeThemeFg(theme, "muted", sectionLabels[section]),
|
|
1153
|
+
);
|
|
1154
|
+
const full = ` ${rendered.join(safeThemeFg(theme, "muted", " / "))}`;
|
|
1155
|
+
if (visibleWidth(full) <= width) return full;
|
|
1156
|
+
return ` ${theme.bold(sectionLabels[active])} (${settingsSections.indexOf(active) + 1}/${settingsSections.length})`;
|
|
1157
|
+
}
|
|
1158
|
+
function withSectionFooter(
|
|
1159
|
+
lines: string[],
|
|
1160
|
+
theme: ExtensionContext["ui"]["theme"],
|
|
1161
|
+
): string[] {
|
|
1162
|
+
const copy = [...lines];
|
|
1163
|
+
for (let index = copy.length - 1; index >= 0; index -= 1) {
|
|
1164
|
+
if (copy[index]?.includes("Enter/Space")) {
|
|
1165
|
+
copy[index] = safeThemeFg(
|
|
1166
|
+
theme,
|
|
1167
|
+
"muted",
|
|
1168
|
+
" Enter/Space to change · Tab/Shift+Tab to switch sections · Esc to close",
|
|
1169
|
+
);
|
|
1170
|
+
break;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return copy;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export function registerShellSettingsCommand(
|
|
1177
|
+
pi: ExtensionAPI,
|
|
1178
|
+
deps: SettingsCommandDeps,
|
|
1179
|
+
): void {
|
|
1180
|
+
const setEditor = (patch: EditorPatch, ctx: ExtensionContext): ApplyResult =>
|
|
1181
|
+
deps.setEditorComponent(patch, ctx);
|
|
1182
|
+
const setMessages = (patch: UserMessagesPatch, ctx: ExtensionContext) => {
|
|
1183
|
+
deps.setUserMessagesComponent(patch, ctx);
|
|
1184
|
+
};
|
|
1185
|
+
const setFooter = (patch: FooterPatch, ctx: ExtensionContext) => {
|
|
1186
|
+
deps.setFooterComponent(patch, ctx);
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
pi.registerCommand("zentui", {
|
|
1190
|
+
description: "Configure Zentui",
|
|
1191
|
+
getArgumentCompletions: argumentCompletions,
|
|
1192
|
+
handler: async (_args, ctx) => {
|
|
1193
|
+
const args = typeof _args === "string" ? _args : "";
|
|
1194
|
+
const format = parseFormatCommand(args);
|
|
1195
|
+
if (format) {
|
|
1196
|
+
try {
|
|
1197
|
+
deps.setFooterFormat(format.value ?? "", ctx);
|
|
1198
|
+
deps.requestRender();
|
|
1199
|
+
if (ctx.hasUI)
|
|
1200
|
+
ctx.ui.notify(
|
|
1201
|
+
format.value === undefined
|
|
1202
|
+
? "Footer format cleared (using default layout)"
|
|
1203
|
+
: `Footer format: ${format.value}`,
|
|
1204
|
+
"info",
|
|
1205
|
+
);
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
if (ctx.hasUI)
|
|
1208
|
+
ctx.ui.notify(
|
|
1209
|
+
`Could not update footer format: ${error instanceof Error ? error.message : String(error)}`,
|
|
1210
|
+
"error",
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const direct = parseDirectOperation(args, deps.getConfig());
|
|
1217
|
+
if (direct) {
|
|
1218
|
+
try {
|
|
1219
|
+
let result: ApplyResult = { applied: true };
|
|
1220
|
+
let label = "";
|
|
1221
|
+
switch (direct.kind) {
|
|
1222
|
+
case "editor":
|
|
1223
|
+
result = setEditor({ enabled: direct.enabled }, ctx);
|
|
1224
|
+
label = "Editor";
|
|
1225
|
+
break;
|
|
1226
|
+
case "messages":
|
|
1227
|
+
setMessages({ enabled: direct.enabled }, ctx);
|
|
1228
|
+
label = "User messages";
|
|
1229
|
+
break;
|
|
1230
|
+
case "footer":
|
|
1231
|
+
setFooter({ style: direct.enabled ? "starship" : "native" }, ctx);
|
|
1232
|
+
label = "Footer";
|
|
1233
|
+
break;
|
|
1234
|
+
case "viewport":
|
|
1235
|
+
result = setEditor({ viewportIndicators: direct.enabled }, ctx);
|
|
1236
|
+
label = "Editor viewport indicators";
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
deps.requestRender();
|
|
1240
|
+
if (ctx.hasUI)
|
|
1241
|
+
ctx.ui.notify(
|
|
1242
|
+
`${label}: ${featureValue(direct.enabled)}${result.applied ? "" : ` (${result.reason ?? "reload Pi to apply this change"})`}`,
|
|
1243
|
+
"info",
|
|
1244
|
+
);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
if (ctx.hasUI)
|
|
1247
|
+
ctx.ui.notify(
|
|
1248
|
+
`Could not update Zentui settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
1249
|
+
"error",
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const initialSection = directSection(args);
|
|
1256
|
+
if (args.trim() && !initialSection) {
|
|
1257
|
+
if (ctx.hasUI) ctx.ui.notify(usageText(), "warning");
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const mode = (ctx as typeof ctx & { mode?: string }).mode;
|
|
1261
|
+
if (!ctx.hasUI || (mode !== undefined && mode !== "tui")) return;
|
|
1262
|
+
|
|
1263
|
+
let requestedSection = initialSection ?? "appearance";
|
|
1264
|
+
let requestedFocusId: string | undefined;
|
|
1265
|
+
while (true) {
|
|
1266
|
+
const initialFocusId = requestedFocusId;
|
|
1267
|
+
requestedFocusId = undefined;
|
|
1268
|
+
const outcome = await ctx.ui.custom<SettingsOutcome>(
|
|
1269
|
+
(tui, theme, _keybindings, done) => {
|
|
1270
|
+
const listTheme = deps.settingsListTheme ?? getSettingsListTheme();
|
|
1271
|
+
let activeSection = requestedSection;
|
|
1272
|
+
let settingsList: SettingsList;
|
|
1273
|
+
let preview: WorkingLineFrames | undefined;
|
|
1274
|
+
let previewFrameIndex = 0;
|
|
1275
|
+
let cancelPreview = () => {};
|
|
1276
|
+
const stopPreview = (reset = true) => {
|
|
1277
|
+
cancelPreview();
|
|
1278
|
+
cancelPreview = () => {};
|
|
1279
|
+
if (reset) {
|
|
1280
|
+
preview = undefined;
|
|
1281
|
+
previewFrameIndex = 0;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
const startPreview = () => {
|
|
1285
|
+
const previous = preview;
|
|
1286
|
+
const previousState = previous?.frameStates[previewFrameIndex];
|
|
1287
|
+
stopPreview(false);
|
|
1288
|
+
if (activeSection !== "workingLine") return;
|
|
1289
|
+
try {
|
|
1290
|
+
const config = deps.getConfig();
|
|
1291
|
+
const spinnerTick = previousState?.spinnerTick ?? 0;
|
|
1292
|
+
let textTick = previousState?.textTick ?? 0;
|
|
1293
|
+
let generated = buildWorkingLinePreviewFrames(
|
|
1294
|
+
config.components.workingLine,
|
|
1295
|
+
config.colors,
|
|
1296
|
+
theme,
|
|
1297
|
+
spinnerTick,
|
|
1298
|
+
textTick,
|
|
1299
|
+
);
|
|
1300
|
+
if (previous && previousState) {
|
|
1301
|
+
textTick = remapWorkingLineTextTick(
|
|
1302
|
+
previous.textAnimation,
|
|
1303
|
+
previous.textWidth,
|
|
1304
|
+
previousState.textTick,
|
|
1305
|
+
generated.textAnimation,
|
|
1306
|
+
generated.textWidth,
|
|
1307
|
+
previous.textOrigin,
|
|
1308
|
+
generated.textOrigin,
|
|
1309
|
+
);
|
|
1310
|
+
generated = buildWorkingLinePreviewFrames(
|
|
1311
|
+
config.components.workingLine,
|
|
1312
|
+
config.colors,
|
|
1313
|
+
theme,
|
|
1314
|
+
spinnerTick,
|
|
1315
|
+
textTick,
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
preview = generated;
|
|
1319
|
+
previewFrameIndex = 0;
|
|
1320
|
+
const advance = () => {
|
|
1321
|
+
if (
|
|
1322
|
+
activeSection !== "workingLine" ||
|
|
1323
|
+
!preview ||
|
|
1324
|
+
preview.frames.length === 0
|
|
1325
|
+
)
|
|
1326
|
+
return;
|
|
1327
|
+
previewFrameIndex =
|
|
1328
|
+
(previewFrameIndex + 1) % preview.frames.length;
|
|
1329
|
+
tui.requestRender();
|
|
1330
|
+
cancelPreview = deps.sessionLifecycle.defer(
|
|
1331
|
+
advance,
|
|
1332
|
+
preview.intervalMs,
|
|
1333
|
+
);
|
|
1334
|
+
};
|
|
1335
|
+
cancelPreview = deps.sessionLifecycle.defer(
|
|
1336
|
+
advance,
|
|
1337
|
+
generated.intervalMs,
|
|
1338
|
+
);
|
|
1339
|
+
} catch {
|
|
1340
|
+
stopPreview();
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
const finishSettings = (result: SettingsOutcome) => {
|
|
1344
|
+
stopPreview();
|
|
1345
|
+
done(result);
|
|
1346
|
+
};
|
|
1347
|
+
const notifyChange = (
|
|
1348
|
+
label: string,
|
|
1349
|
+
value: string,
|
|
1350
|
+
result?: ApplyResult,
|
|
1351
|
+
) => {
|
|
1352
|
+
deps.requestRender();
|
|
1353
|
+
ctx.ui.notify(
|
|
1354
|
+
`${label}: ${value}${result && !result.applied ? ` (${result.reason ?? "reload Pi to apply this change"})` : ""}`,
|
|
1355
|
+
"info",
|
|
1356
|
+
);
|
|
1357
|
+
startPreview();
|
|
1358
|
+
tui.requestRender();
|
|
1359
|
+
};
|
|
1360
|
+
const notifyWorkingLineChange = (
|
|
1361
|
+
label: string,
|
|
1362
|
+
value: string,
|
|
1363
|
+
result: ApplyResult,
|
|
1364
|
+
previewChanged = true,
|
|
1365
|
+
) => {
|
|
1366
|
+
ctx.ui.notify(
|
|
1367
|
+
`${label}: ${value}${result.applied ? "" : ` (${result.reason ?? "reload Pi to apply this change"})`}`,
|
|
1368
|
+
"info",
|
|
1369
|
+
);
|
|
1370
|
+
if (previewChanged) startPreview();
|
|
1371
|
+
tui.requestRender();
|
|
1372
|
+
};
|
|
1373
|
+
const makeSettingsList = (focusId?: string): SettingsList => {
|
|
1374
|
+
const items = buildSectionItems(
|
|
1375
|
+
activeSection,
|
|
1376
|
+
deps.getConfig(),
|
|
1377
|
+
deps.getActiveExtensionStatuses(),
|
|
1378
|
+
);
|
|
1379
|
+
const list = new SettingsList(
|
|
1380
|
+
items,
|
|
1381
|
+
8,
|
|
1382
|
+
listTheme,
|
|
1383
|
+
(id, newValue) => {
|
|
1384
|
+
try {
|
|
1385
|
+
const enabled = isFeatureState(newValue)
|
|
1386
|
+
? newValue === "enabled"
|
|
1387
|
+
: undefined;
|
|
1388
|
+
if (id === "editorEnabled" && enabled !== undefined) {
|
|
1389
|
+
finishSettings("close");
|
|
1390
|
+
deps.sessionLifecycle.defer(() => {
|
|
1391
|
+
try {
|
|
1392
|
+
const result = setEditor({ enabled }, ctx);
|
|
1393
|
+
deps.requestRender();
|
|
1394
|
+
ctx.ui.notify(
|
|
1395
|
+
`Editor: ${newValue}${result.applied ? "" : ` (${result.reason ?? "reload Pi to apply this change"})`}`,
|
|
1396
|
+
"info",
|
|
1397
|
+
);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
ctx.ui.notify(
|
|
1400
|
+
`Could not update Zentui settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
1401
|
+
"error",
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const selectedEditorStyle =
|
|
1408
|
+
id === "editorStyle"
|
|
1409
|
+
? editorStyleId(newValue)
|
|
1410
|
+
: undefined;
|
|
1411
|
+
if (selectedEditorStyle) {
|
|
1412
|
+
setEditor({ style: selectedEditorStyle }, ctx);
|
|
1413
|
+
settingsList = makeSettingsList("editorStyle");
|
|
1414
|
+
notifyChange("Editor style", newValue);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (id === "editorColorSource" && isColorSource(newValue)) {
|
|
1418
|
+
setEditor({ colorSource: newValue }, ctx);
|
|
1419
|
+
settingsList.updateValue(id, newValue);
|
|
1420
|
+
notifyChange("Editor colors", newValue);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (
|
|
1424
|
+
id === "editorModelLabel" &&
|
|
1425
|
+
(newValue === "id" || newValue === "name")
|
|
1426
|
+
) {
|
|
1427
|
+
setEditor({ modelLabel: newValue }, ctx);
|
|
1428
|
+
settingsList.updateValue(id, newValue);
|
|
1429
|
+
notifyChange("Editor model label", newValue);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
if (
|
|
1433
|
+
id === "editorBorderColorMode" &&
|
|
1434
|
+
(newValue === "static" || newValue === "adaptive")
|
|
1435
|
+
) {
|
|
1436
|
+
setEditor({ borderColorMode: newValue }, ctx);
|
|
1437
|
+
settingsList.updateValue(id, newValue);
|
|
1438
|
+
notifyChange("Editor border color", newValue);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (
|
|
1442
|
+
id === "editorViewportIndicators" &&
|
|
1443
|
+
enabled !== undefined
|
|
1444
|
+
) {
|
|
1445
|
+
setEditor({ viewportIndicators: enabled }, ctx);
|
|
1446
|
+
settingsList.updateValue(id, newValue);
|
|
1447
|
+
notifyChange("Editor viewport indicators", newValue);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if (
|
|
1451
|
+
id === "opencodeCompletionMenu" &&
|
|
1452
|
+
(newValue === "native" || newValue === "palette")
|
|
1453
|
+
) {
|
|
1454
|
+
const style = deps.getConfig().components.editor.style;
|
|
1455
|
+
if (style === "opencode")
|
|
1456
|
+
deps.setPolished({ completionMenu: newValue }, ctx);
|
|
1457
|
+
else if (style === "opencode-copy-friendly")
|
|
1458
|
+
deps.setPolishedCopyFriendly(
|
|
1459
|
+
{ completionMenu: newValue },
|
|
1460
|
+
ctx,
|
|
1461
|
+
);
|
|
1462
|
+
else return;
|
|
1463
|
+
settingsList.updateValue(id, newValue);
|
|
1464
|
+
notifyChange("Completion menu", newValue);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (
|
|
1468
|
+
id === "accentRailSurface" &&
|
|
1469
|
+
(newValue === "filled" || newValue === "transparent")
|
|
1470
|
+
) {
|
|
1471
|
+
deps.setAccentRail(
|
|
1472
|
+
{ transparent: newValue === "transparent" },
|
|
1473
|
+
ctx,
|
|
1474
|
+
);
|
|
1475
|
+
settingsList.updateValue(id, newValue);
|
|
1476
|
+
notifyChange("Accent Rail surface", newValue);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (id.startsWith("minimalist")) {
|
|
1480
|
+
if (
|
|
1481
|
+
id === "minimalistPathDisplay" &&
|
|
1482
|
+
["compact", "project", "full"].includes(newValue)
|
|
1483
|
+
)
|
|
1484
|
+
deps.setMinimalist(
|
|
1485
|
+
{
|
|
1486
|
+
pathDisplay:
|
|
1487
|
+
newValue as MinimalistConfig["pathDisplay"],
|
|
1488
|
+
},
|
|
1489
|
+
ctx,
|
|
1490
|
+
);
|
|
1491
|
+
else if (
|
|
1492
|
+
id === "minimalistContextFormat" &&
|
|
1493
|
+
["percent", "percent-total"].includes(newValue)
|
|
1494
|
+
)
|
|
1495
|
+
deps.setMinimalist(
|
|
1496
|
+
{
|
|
1497
|
+
contextFormat:
|
|
1498
|
+
newValue as MinimalistConfig["contextFormat"],
|
|
1499
|
+
},
|
|
1500
|
+
ctx,
|
|
1501
|
+
);
|
|
1502
|
+
else if (enabled !== undefined) {
|
|
1503
|
+
const key =
|
|
1504
|
+
id === "minimalistContextGauge"
|
|
1505
|
+
? "contextGauge"
|
|
1506
|
+
: id === "minimalistShowSessionName"
|
|
1507
|
+
? "showSessionName"
|
|
1508
|
+
: id === "minimalistShowTimer"
|
|
1509
|
+
? "showTimer"
|
|
1510
|
+
: id === "minimalistShowCost"
|
|
1511
|
+
? "showCost"
|
|
1512
|
+
: id === "minimalistShowGit"
|
|
1513
|
+
? "showGit"
|
|
1514
|
+
: undefined;
|
|
1515
|
+
if (!key) return;
|
|
1516
|
+
deps.setMinimalist({ [key]: enabled }, ctx);
|
|
1517
|
+
} else return;
|
|
1518
|
+
settingsList.updateValue(id, newValue);
|
|
1519
|
+
notifyChange(id, newValue);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (id === "userMessagesEnabled" && enabled !== undefined) {
|
|
1524
|
+
setMessages({ enabled }, ctx);
|
|
1525
|
+
settingsList.updateValue(id, newValue);
|
|
1526
|
+
notifyChange("User messages", newValue);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const selectedMessageStyle =
|
|
1530
|
+
id === "userMessagesStyle"
|
|
1531
|
+
? userMessageStyleId(newValue)
|
|
1532
|
+
: undefined;
|
|
1533
|
+
if (selectedMessageStyle) {
|
|
1534
|
+
setMessages({ style: selectedMessageStyle }, ctx);
|
|
1535
|
+
settingsList = makeSettingsList("userMessagesStyle");
|
|
1536
|
+
notifyChange("Message style", newValue);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (
|
|
1540
|
+
id === "userMessagesColorSource" &&
|
|
1541
|
+
isColorSource(newValue)
|
|
1542
|
+
) {
|
|
1543
|
+
setMessages({ colorSource: newValue }, ctx);
|
|
1544
|
+
settingsList.updateValue(id, newValue);
|
|
1545
|
+
notifyChange("Message colors", newValue);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
if (id === "workingLineEnabled" && enabled !== undefined) {
|
|
1549
|
+
const result = deps.setWorkingLineComponent(
|
|
1550
|
+
{ enabled },
|
|
1551
|
+
ctx,
|
|
1552
|
+
);
|
|
1553
|
+
settingsList.updateValue(id, newValue);
|
|
1554
|
+
notifyWorkingLineChange("Working line", newValue, result);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (
|
|
1558
|
+
id === "workingLineTurnSummary" &&
|
|
1559
|
+
enabled !== undefined
|
|
1560
|
+
) {
|
|
1561
|
+
const result = deps.setWorkingLineComponent(
|
|
1562
|
+
{ turnSummary: enabled },
|
|
1563
|
+
ctx,
|
|
1564
|
+
);
|
|
1565
|
+
settingsList.updateValue(id, newValue);
|
|
1566
|
+
notifyWorkingLineChange(
|
|
1567
|
+
"Turn summary",
|
|
1568
|
+
newValue,
|
|
1569
|
+
result,
|
|
1570
|
+
false,
|
|
1571
|
+
);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const selectedWorkingLineSpinner =
|
|
1575
|
+
id === "workingLineSpinner"
|
|
1576
|
+
? workingLineSpinnerId(newValue)
|
|
1577
|
+
: undefined;
|
|
1578
|
+
if (selectedWorkingLineSpinner) {
|
|
1579
|
+
const result = deps.setWorkingLineComponent(
|
|
1580
|
+
{ spinner: selectedWorkingLineSpinner },
|
|
1581
|
+
ctx,
|
|
1582
|
+
);
|
|
1583
|
+
settingsList.updateValue(id, newValue);
|
|
1584
|
+
notifyWorkingLineChange("Spinner", newValue, result);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (
|
|
1588
|
+
id === "workingLineSpinnerSpeed" ||
|
|
1589
|
+
id === "workingLineTextSpeed"
|
|
1590
|
+
) {
|
|
1591
|
+
const spinnerSpeed = id === "workingLineSpinnerSpeed";
|
|
1592
|
+
if (newValue === "Custom…") {
|
|
1593
|
+
finishSettings(
|
|
1594
|
+
spinnerSpeed
|
|
1595
|
+
? "edit-working-line-spinner-speed"
|
|
1596
|
+
: "edit-working-line-text-speed",
|
|
1597
|
+
);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const presets = spinnerSpeed
|
|
1601
|
+
? workingLineSpinnerSpeedPresets
|
|
1602
|
+
: workingLineTextSpeedPresets;
|
|
1603
|
+
const intervalMs = presets.find(
|
|
1604
|
+
(preset) => preset.label === newValue,
|
|
1605
|
+
)?.intervalMs;
|
|
1606
|
+
if (intervalMs !== undefined) {
|
|
1607
|
+
const result = deps.setWorkingLineComponent(
|
|
1608
|
+
spinnerSpeed
|
|
1609
|
+
? { spinnerIntervalMs: intervalMs }
|
|
1610
|
+
: { textIntervalMs: intervalMs },
|
|
1611
|
+
ctx,
|
|
1612
|
+
);
|
|
1613
|
+
settingsList.updateValue(id, newValue);
|
|
1614
|
+
notifyWorkingLineChange(
|
|
1615
|
+
spinnerSpeed ? "Spinner speed" : "Text motion speed",
|
|
1616
|
+
`${intervalMs} ms`,
|
|
1617
|
+
result,
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (
|
|
1623
|
+
id === "workingLineTextAnimation" &&
|
|
1624
|
+
workingLineTextAnimationValues.includes(
|
|
1625
|
+
newValue as WorkingLineTextAnimation,
|
|
1626
|
+
)
|
|
1627
|
+
) {
|
|
1628
|
+
const result = deps.setWorkingLineComponent(
|
|
1629
|
+
{ textAnimation: newValue as WorkingLineTextAnimation },
|
|
1630
|
+
ctx,
|
|
1631
|
+
);
|
|
1632
|
+
settingsList.updateValue(id, newValue);
|
|
1633
|
+
notifyWorkingLineChange(
|
|
1634
|
+
"Text animation",
|
|
1635
|
+
newValue,
|
|
1636
|
+
result,
|
|
1637
|
+
);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (
|
|
1641
|
+
id === "workingLineColorSource" &&
|
|
1642
|
+
isColorSource(newValue)
|
|
1643
|
+
) {
|
|
1644
|
+
const result = deps.setWorkingLineComponent(
|
|
1645
|
+
{ colorSource: newValue },
|
|
1646
|
+
ctx,
|
|
1647
|
+
);
|
|
1648
|
+
settingsList.updateValue(id, newValue);
|
|
1649
|
+
notifyWorkingLineChange("Color source", newValue, result);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
if (
|
|
1653
|
+
id === "workingLineAnimateSpinnerColor" &&
|
|
1654
|
+
enabled !== undefined
|
|
1655
|
+
) {
|
|
1656
|
+
const result = deps.setWorkingLineComponent(
|
|
1657
|
+
{ animateSpinnerColor: enabled },
|
|
1658
|
+
ctx,
|
|
1659
|
+
);
|
|
1660
|
+
settingsList.updateValue(id, newValue);
|
|
1661
|
+
notifyWorkingLineChange(
|
|
1662
|
+
"Animate spinner color",
|
|
1663
|
+
newValue,
|
|
1664
|
+
result,
|
|
1665
|
+
);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
if (
|
|
1669
|
+
id === "workingLineCustomMessages" &&
|
|
1670
|
+
enabled !== undefined
|
|
1671
|
+
) {
|
|
1672
|
+
const result = deps.setWorkingLineComponent(
|
|
1673
|
+
{ messages: { custom: enabled } },
|
|
1674
|
+
ctx,
|
|
1675
|
+
);
|
|
1676
|
+
settingsList.updateValue(id, newValue);
|
|
1677
|
+
notifyWorkingLineChange(
|
|
1678
|
+
"Custom messages",
|
|
1679
|
+
newValue,
|
|
1680
|
+
result,
|
|
1681
|
+
);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (
|
|
1685
|
+
(id === "workingLineTool" ||
|
|
1686
|
+
id === "workingLineElapsed" ||
|
|
1687
|
+
id === "workingLineThought" ||
|
|
1688
|
+
id === "workingLineTokens") &&
|
|
1689
|
+
enabled !== undefined
|
|
1690
|
+
) {
|
|
1691
|
+
const key =
|
|
1692
|
+
id === "workingLineTool"
|
|
1693
|
+
? "tool"
|
|
1694
|
+
: id === "workingLineElapsed"
|
|
1695
|
+
? "elapsed"
|
|
1696
|
+
: id === "workingLineThought"
|
|
1697
|
+
? "thought"
|
|
1698
|
+
: "tokens";
|
|
1699
|
+
const result = deps.setWorkingLineComponent(
|
|
1700
|
+
{ segments: { [key]: enabled } },
|
|
1701
|
+
ctx,
|
|
1702
|
+
);
|
|
1703
|
+
settingsList.updateValue(id, newValue);
|
|
1704
|
+
notifyWorkingLineChange(
|
|
1705
|
+
id.slice("workingLine".length),
|
|
1706
|
+
newValue,
|
|
1707
|
+
result,
|
|
1708
|
+
);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (id === "workingLineMessageList") {
|
|
1712
|
+
finishSettings("edit-working-line-messages");
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (
|
|
1716
|
+
id === "selectorBordersEnabled" &&
|
|
1717
|
+
enabled !== undefined
|
|
1718
|
+
) {
|
|
1719
|
+
deps.setSelectorBordersComponent({ enabled }, ctx);
|
|
1720
|
+
settingsList.updateValue(id, newValue);
|
|
1721
|
+
notifyChange("Selector borders", newValue);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (
|
|
1725
|
+
id === "selectorBordersStyle" &&
|
|
1726
|
+
newValue === "zentui"
|
|
1727
|
+
) {
|
|
1728
|
+
deps.setSelectorBordersComponent(
|
|
1729
|
+
{ style: newValue },
|
|
1730
|
+
ctx,
|
|
1731
|
+
);
|
|
1732
|
+
settingsList.updateValue(id, newValue);
|
|
1733
|
+
notifyChange("Selector border style", newValue);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (
|
|
1737
|
+
id === "selectorBordersColorSource" &&
|
|
1738
|
+
isColorSource(newValue)
|
|
1739
|
+
) {
|
|
1740
|
+
deps.setSelectorBordersComponent(
|
|
1741
|
+
{ colorSource: newValue },
|
|
1742
|
+
ctx,
|
|
1743
|
+
);
|
|
1744
|
+
settingsList.updateValue(id, newValue);
|
|
1745
|
+
notifyChange("Selector border colors", newValue);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (id === "iconMode" && isIconMode(newValue)) {
|
|
1749
|
+
deps.setIconMode(newValue);
|
|
1750
|
+
settingsList.updateValue(id, newValue);
|
|
1751
|
+
notifyChange("Icon mode", newValue);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const selectedFooterStyle =
|
|
1756
|
+
id === "footerStyle"
|
|
1757
|
+
? footerStyleId(newValue)
|
|
1758
|
+
: undefined;
|
|
1759
|
+
if (selectedFooterStyle) {
|
|
1760
|
+
setFooter({ style: selectedFooterStyle }, ctx);
|
|
1761
|
+
settingsList = makeSettingsList("footerStyle");
|
|
1762
|
+
notifyChange("Footer style", newValue);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
if (id === "footerColorSource" && isColorSource(newValue)) {
|
|
1766
|
+
setFooter({ colorSource: newValue }, ctx);
|
|
1767
|
+
settingsList.updateValue(id, newValue);
|
|
1768
|
+
notifyChange("Footer colors", newValue);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (
|
|
1772
|
+
id === "footerModelLabel" &&
|
|
1773
|
+
(newValue === "id" || newValue === "name")
|
|
1774
|
+
) {
|
|
1775
|
+
setFooter({ modelLabel: newValue }, ctx);
|
|
1776
|
+
settingsList.updateValue(id, newValue);
|
|
1777
|
+
notifyChange("Footer model label", newValue);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (id === "responsiveFooter" && enabled !== undefined) {
|
|
1782
|
+
deps.setResponsiveFooter(
|
|
1783
|
+
{ responsiveFooter: enabled },
|
|
1784
|
+
ctx,
|
|
1785
|
+
);
|
|
1786
|
+
settingsList.updateValue(id, newValue);
|
|
1787
|
+
notifyChange("Responsive footer", newValue);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (
|
|
1791
|
+
id === "compactFooterMaxLines" &&
|
|
1792
|
+
compactFooterMaxLineValues.includes(newValue as never)
|
|
1793
|
+
) {
|
|
1794
|
+
const value: CompactFooterMaxLines =
|
|
1795
|
+
newValue === "unlimited"
|
|
1796
|
+
? "unlimited"
|
|
1797
|
+
: (Number(newValue) as 1 | 2 | 3);
|
|
1798
|
+
deps.setResponsiveFooter(
|
|
1799
|
+
{ compactFooterMaxLines: value },
|
|
1800
|
+
ctx,
|
|
1801
|
+
);
|
|
1802
|
+
settingsList.updateValue(id, newValue);
|
|
1803
|
+
notifyChange("Compact footer rows", newValue);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (
|
|
1807
|
+
id === "contextStyle" &&
|
|
1808
|
+
contextStyleValues.includes(newValue as ContextStyle)
|
|
1809
|
+
) {
|
|
1810
|
+
deps.setContextStyle(newValue as ContextStyle);
|
|
1811
|
+
settingsList.updateValue(id, newValue);
|
|
1812
|
+
notifyChange("Context style", newValue);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
if (id === "separator" && isSeparatorStyle(newValue)) {
|
|
1816
|
+
deps.setSeparator(newValue);
|
|
1817
|
+
settingsList.updateValue(id, newValue);
|
|
1818
|
+
notifyChange("Separator", newValue);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (
|
|
1822
|
+
id === "pathDisplay" &&
|
|
1823
|
+
pathDisplayModeValues.includes(newValue as never)
|
|
1824
|
+
) {
|
|
1825
|
+
deps.setPathDisplay({
|
|
1826
|
+
mode: newValue as PathDisplayConfig["mode"],
|
|
1827
|
+
});
|
|
1828
|
+
settingsList.updateValue(id, newValue);
|
|
1829
|
+
notifyChange("Path display", newValue);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (
|
|
1833
|
+
id === "pathDepth" &&
|
|
1834
|
+
pathDepthValues.includes(newValue as never)
|
|
1835
|
+
) {
|
|
1836
|
+
deps.setPathDisplay({ depth: Number(newValue) });
|
|
1837
|
+
settingsList.updateValue(id, newValue);
|
|
1838
|
+
notifyChange("Path depth", newValue);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const segment = footerSegmentSettingFromId(id);
|
|
1843
|
+
if (segment && enabled !== undefined) {
|
|
1844
|
+
deps.setFooterSegments({ [segment]: enabled }, ctx);
|
|
1845
|
+
settingsList.updateValue(id, newValue);
|
|
1846
|
+
notifyChange(
|
|
1847
|
+
footerSegmentSettingLabels[segment],
|
|
1848
|
+
newValue,
|
|
1849
|
+
);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (id === "branchLength") {
|
|
1853
|
+
const value =
|
|
1854
|
+
newValue === "full" ? "full" : Number(newValue);
|
|
1855
|
+
if (
|
|
1856
|
+
value !== "full" &&
|
|
1857
|
+
(!Number.isInteger(value) || value <= 0)
|
|
1858
|
+
)
|
|
1859
|
+
return;
|
|
1860
|
+
deps.setGitBranch({ maxLength: value });
|
|
1861
|
+
settingsList.updateValue(id, newValue);
|
|
1862
|
+
notifyChange("Branch length", newValue);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
if (
|
|
1866
|
+
id === "gitCommitOnlyDetached" &&
|
|
1867
|
+
enabled !== undefined
|
|
1868
|
+
) {
|
|
1869
|
+
deps.setGitCommit({ onlyDetached: enabled }, ctx);
|
|
1870
|
+
settingsList.updateValue(id, newValue);
|
|
1871
|
+
notifyChange("Commit only on detached HEAD", newValue);
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (id === "gitCommitShowTag" && enabled !== undefined) {
|
|
1875
|
+
deps.setGitCommit({ showTag: enabled }, ctx);
|
|
1876
|
+
settingsList.updateValue(id, newValue);
|
|
1877
|
+
notifyChange("Show exact-match tag", newValue);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (
|
|
1881
|
+
id === "gitMetricsOnlyNonzero" &&
|
|
1882
|
+
enabled !== undefined
|
|
1883
|
+
) {
|
|
1884
|
+
deps.setGitMetrics({ onlyNonzero: enabled }, ctx);
|
|
1885
|
+
settingsList.updateValue(id, newValue);
|
|
1886
|
+
notifyChange("Hide zero metrics", newValue);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (
|
|
1890
|
+
id === "gitMetricsIgnoreSubmodules" &&
|
|
1891
|
+
enabled !== undefined
|
|
1892
|
+
) {
|
|
1893
|
+
deps.setGitMetrics({ ignoreSubmodules: enabled }, ctx);
|
|
1894
|
+
settingsList.updateValue(id, newValue);
|
|
1895
|
+
notifyChange("Ignore submodules", newValue);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
if (
|
|
1900
|
+
id === "extensionStatusDefaultPlacement" &&
|
|
1901
|
+
isExtensionStatusPlacement(newValue)
|
|
1902
|
+
) {
|
|
1903
|
+
deps.setExtensionStatusDefaultPlacement(newValue);
|
|
1904
|
+
settingsList = makeSettingsList(
|
|
1905
|
+
"extensionStatusDefaultPlacement",
|
|
1906
|
+
);
|
|
1907
|
+
notifyChange(
|
|
1908
|
+
"Default extension status placement",
|
|
1909
|
+
newValue,
|
|
1910
|
+
);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const thirdParty = thirdPartyStatusSettingFromId(id);
|
|
1914
|
+
if (
|
|
1915
|
+
thirdParty?.kind === "placement" &&
|
|
1916
|
+
isExtensionStatusPlacement(newValue)
|
|
1917
|
+
) {
|
|
1918
|
+
deps.setExtensionStatusPlacement(
|
|
1919
|
+
thirdParty.key,
|
|
1920
|
+
newValue,
|
|
1921
|
+
);
|
|
1922
|
+
settingsList.updateValue(id, newValue);
|
|
1923
|
+
notifyChange(
|
|
1924
|
+
`Third-party status ${thirdParty.key} placement`,
|
|
1925
|
+
newValue,
|
|
1926
|
+
);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (
|
|
1930
|
+
thirdParty?.kind === "colorMode" &&
|
|
1931
|
+
isExtensionStatusColorMode(newValue)
|
|
1932
|
+
) {
|
|
1933
|
+
deps.setExtensionStatusColorMode(
|
|
1934
|
+
thirdParty.key,
|
|
1935
|
+
newValue,
|
|
1936
|
+
);
|
|
1937
|
+
settingsList.updateValue(id, newValue);
|
|
1938
|
+
notifyChange(
|
|
1939
|
+
`Third-party status ${thirdParty.key} color`,
|
|
1940
|
+
newValue,
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
stopPreview();
|
|
1945
|
+
settingsList = makeSettingsList(id);
|
|
1946
|
+
tui.requestRender();
|
|
1947
|
+
ctx.ui.notify(
|
|
1948
|
+
`Could not update Zentui settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
1949
|
+
"error",
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1953
|
+
() => finishSettings("close"),
|
|
1954
|
+
);
|
|
1955
|
+
if (focusId) {
|
|
1956
|
+
const target = items.findIndex((item) => item.id === focusId);
|
|
1957
|
+
for (let index = 0; index < target; index += 1)
|
|
1958
|
+
list.handleInput("\x1b[B");
|
|
1959
|
+
}
|
|
1960
|
+
return list;
|
|
1961
|
+
};
|
|
1962
|
+
settingsList = makeSettingsList(initialFocusId);
|
|
1963
|
+
startPreview();
|
|
1964
|
+
const renderPreviewRows = (previewWidth: number): string[] => {
|
|
1965
|
+
if (previewWidth <= 0) return [];
|
|
1966
|
+
if (activeSection === "editor")
|
|
1967
|
+
return renderEditorSettingsPreview(
|
|
1968
|
+
deps.getConfig(),
|
|
1969
|
+
theme,
|
|
1970
|
+
previewWidth,
|
|
1971
|
+
);
|
|
1972
|
+
if (activeSection === "userMessages")
|
|
1973
|
+
return renderUserMessageSettingsPreview(
|
|
1974
|
+
deps.getConfig(),
|
|
1975
|
+
theme,
|
|
1976
|
+
previewWidth,
|
|
1977
|
+
);
|
|
1978
|
+
if (
|
|
1979
|
+
activeSection === "workingLine" &&
|
|
1980
|
+
preview &&
|
|
1981
|
+
preview.frames.length > 0
|
|
1982
|
+
)
|
|
1983
|
+
return [
|
|
1984
|
+
truncateToWidth(
|
|
1985
|
+
preview.frames[previewFrameIndex] ?? preview.frames[0],
|
|
1986
|
+
Math.min(SETTINGS_PREVIEW_MAX_WIDTH, previewWidth),
|
|
1987
|
+
"",
|
|
1988
|
+
),
|
|
1989
|
+
];
|
|
1990
|
+
return [];
|
|
1991
|
+
};
|
|
1992
|
+
return {
|
|
1993
|
+
render(width: number) {
|
|
1994
|
+
const border = renderChromeBorder(
|
|
1995
|
+
theme,
|
|
1996
|
+
deps.getConfig().components.selectorBorders.colorSource,
|
|
1997
|
+
EDITOR_BORDER_STYLE,
|
|
1998
|
+
"─".repeat(Math.max(0, width)),
|
|
1999
|
+
);
|
|
2000
|
+
const settingsRows = withSectionFooter(
|
|
2001
|
+
settingsList.render(width),
|
|
2002
|
+
theme,
|
|
2003
|
+
).map((line) => truncateToWidth(line, width, ""));
|
|
2004
|
+
const previewRows = renderPreviewRows(Math.max(0, width - 4));
|
|
2005
|
+
while (
|
|
2006
|
+
previewRows.length > 0 &&
|
|
2007
|
+
visibleWidth(previewRows.at(-1) ?? "") === 0
|
|
2008
|
+
)
|
|
2009
|
+
previewRows.pop();
|
|
2010
|
+
const indentedPreviewRows = previewRows.map((line) =>
|
|
2011
|
+
truncateToWidth(` ${line}`, width, ""),
|
|
2012
|
+
);
|
|
2013
|
+
const bodyRows = indentedPreviewRows.some(
|
|
2014
|
+
(line) => visibleWidth(line) > 0,
|
|
2015
|
+
)
|
|
2016
|
+
? ["", ...indentedPreviewRows, "", ...settingsRows]
|
|
2017
|
+
: settingsRows;
|
|
2018
|
+
return [
|
|
2019
|
+
truncateToWidth(border, width, ""),
|
|
2020
|
+
truncateToWidth(
|
|
2021
|
+
formatSectionTabs(activeSection, theme, width),
|
|
2022
|
+
width,
|
|
2023
|
+
"",
|
|
2024
|
+
),
|
|
2025
|
+
truncateToWidth(border, width, ""),
|
|
2026
|
+
...bodyRows,
|
|
2027
|
+
truncateToWidth(border, width, ""),
|
|
2028
|
+
];
|
|
2029
|
+
},
|
|
2030
|
+
invalidate() {
|
|
2031
|
+
settingsList.invalidate();
|
|
2032
|
+
},
|
|
2033
|
+
handleInput(data: string) {
|
|
2034
|
+
if (matchesKey(data, Key.tab)) {
|
|
2035
|
+
stopPreview();
|
|
2036
|
+
activeSection = nextSection(activeSection);
|
|
2037
|
+
settingsList = makeSettingsList();
|
|
2038
|
+
startPreview();
|
|
2039
|
+
tui.requestRender();
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (matchesKey(data, Key.shift("tab"))) {
|
|
2043
|
+
stopPreview();
|
|
2044
|
+
activeSection = previousSection(activeSection);
|
|
2045
|
+
settingsList = makeSettingsList();
|
|
2046
|
+
startPreview();
|
|
2047
|
+
tui.requestRender();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
settingsList.handleInput(data);
|
|
2051
|
+
},
|
|
2052
|
+
dispose() {
|
|
2053
|
+
stopPreview();
|
|
2054
|
+
},
|
|
2055
|
+
};
|
|
2056
|
+
},
|
|
2057
|
+
);
|
|
2058
|
+
if (outcome === "close" || outcome === undefined) return;
|
|
2059
|
+
if (
|
|
2060
|
+
outcome === "edit-working-line-spinner-speed" ||
|
|
2061
|
+
outcome === "edit-working-line-text-speed"
|
|
2062
|
+
) {
|
|
2063
|
+
const spinnerSpeed = outcome === "edit-working-line-spinner-speed";
|
|
2064
|
+
const workingLine = deps.getConfig().components.workingLine;
|
|
2065
|
+
const before = spinnerSpeed
|
|
2066
|
+
? workingLine.spinnerIntervalMs
|
|
2067
|
+
: workingLine.textIntervalMs;
|
|
2068
|
+
const label = spinnerSpeed ? "Spinner speed" : "Text motion speed";
|
|
2069
|
+
try {
|
|
2070
|
+
const edited = await ctx.ui.input(
|
|
2071
|
+
`${label} (${MIN_WORKING_LINE_INTERVAL_MS}–${MAX_WORKING_LINE_INTERVAL_MS} ms)`,
|
|
2072
|
+
String(before),
|
|
2073
|
+
);
|
|
2074
|
+
if (edited === undefined) {
|
|
2075
|
+
ctx.ui.notify(`${label} unchanged (input canceled)`, "info");
|
|
2076
|
+
} else {
|
|
2077
|
+
const trimmed = edited.trim();
|
|
2078
|
+
const intervalMs = /^[+-]?\d+$/.test(trimmed)
|
|
2079
|
+
? Number(trimmed)
|
|
2080
|
+
: Number.NaN;
|
|
2081
|
+
if (!isValidWorkingLineIntervalMs(intervalMs)) {
|
|
2082
|
+
ctx.ui.notify(
|
|
2083
|
+
`${label} must be a whole number from ${MIN_WORKING_LINE_INTERVAL_MS} to ${MAX_WORKING_LINE_INTERVAL_MS} ms; unchanged.`,
|
|
2084
|
+
"warning",
|
|
2085
|
+
);
|
|
2086
|
+
} else {
|
|
2087
|
+
const result = deps.setWorkingLineComponent(
|
|
2088
|
+
spinnerSpeed
|
|
2089
|
+
? { spinnerIntervalMs: intervalMs }
|
|
2090
|
+
: { textIntervalMs: intervalMs },
|
|
2091
|
+
ctx,
|
|
2092
|
+
);
|
|
2093
|
+
ctx.ui.notify(
|
|
2094
|
+
`${label}: ${intervalMs} ms${result.applied ? "" : ` (${result.reason ?? "reload Pi to apply this change"})`}`,
|
|
2095
|
+
"info",
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
ctx.ui.notify(
|
|
2101
|
+
`Could not update Zentui settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
2102
|
+
"error",
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
requestedSection = "workingLine";
|
|
2106
|
+
requestedFocusId = spinnerSpeed
|
|
2107
|
+
? "workingLineSpinnerSpeed"
|
|
2108
|
+
: "workingLineTextSpeed";
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
try {
|
|
2112
|
+
const before = deps
|
|
2113
|
+
.getConfig()
|
|
2114
|
+
.components.workingLine.messages.values.join("\n");
|
|
2115
|
+
const edited = await ctx.ui.editor(
|
|
2116
|
+
"Working line message list",
|
|
2117
|
+
before,
|
|
2118
|
+
);
|
|
2119
|
+
if (edited !== undefined) {
|
|
2120
|
+
const values = normalizeWorkingLineMessages(edited.split(/\r?\n/));
|
|
2121
|
+
const result = deps.setWorkingLineComponent(
|
|
2122
|
+
{ messages: { values } },
|
|
2123
|
+
ctx,
|
|
2124
|
+
);
|
|
2125
|
+
ctx.ui.notify(
|
|
2126
|
+
`Message list: ${values.length}${values.length === 0 ? " (using styled Working…)" : ""}${result.applied ? "" : ` (${result.reason ?? "reload Pi to apply this change"})`}`,
|
|
2127
|
+
"info",
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
ctx.ui.notify(
|
|
2132
|
+
`Could not update Zentui settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
2133
|
+
"error",
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
requestedSection = "workingLine";
|
|
2137
|
+
requestedFocusId = "workingLineMessageList";
|
|
2138
|
+
}
|
|
2139
|
+
},
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/** Compatibility export for upstream regression tests and migration tooling. */
|
|
2144
|
+
export const registerZentuiSettingsCommand = registerShellSettingsCommand;
|