@narumitw/pi-statusline 0.50.0 → 0.50.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/src/commands.ts CHANGED
@@ -1,937 +1,878 @@
1
1
  import {
2
- DynamicBorder,
3
- type ExtensionAPI,
4
- type ExtensionCommandContext,
5
- type KeybindingsManager,
2
+ DynamicBorder,
3
+ type ExtensionAPI,
4
+ type ExtensionCommandContext,
5
+ type KeybindingsManager,
6
6
  } from "@earendil-works/pi-coding-agent";
7
- import {
8
- Container,
9
- Key,
10
- matchesKey,
11
- Text,
12
- truncateToWidth,
13
- wrapTextWithAnsi,
14
- } from "@earendil-works/pi-tui";
7
+ import { Container, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
15
8
  import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
16
9
  import { completeStatuslineArguments } from "./command-contract.js";
17
10
  import {
18
- INFORMATION_PROFILE_NAMES,
19
- INFORMATION_PROFILES,
20
- type InformationProfileName,
21
- inferInformationProfile,
11
+ INFORMATION_PROFILE_NAMES,
12
+ INFORMATION_PROFILES,
13
+ type InformationProfileName,
14
+ inferInformationProfile,
22
15
  } from "./information-profiles.js";
23
16
  import { segmentPaletteForPreset } from "./presets/index.js";
24
17
  import {
25
- DEFAULT_STATUSLINE_DOCUMENT,
26
- type LoadedStatuslineSettings,
27
- removeStatuslineSettingsDocumentIfMatches,
28
- saveStatuslineSettingsDocument,
18
+ DEFAULT_STATUSLINE_DOCUMENT,
19
+ type LoadedStatuslineSettings,
20
+ removeStatuslineSettingsDocumentIfMatches,
21
+ saveStatuslineSettingsDocument,
29
22
  } from "./settings.js";
30
23
  import {
31
- type ConfigSegmentName,
32
- LINE_BREAK_SEGMENT_NAME,
33
- PALETTE_NAMES,
34
- PALETTE_PRESET_NAMES,
35
- type PaletteName,
36
- type PalettePreset,
37
- SEGMENT_NAMES,
38
- type SegmentName,
24
+ type ConfigSegmentName,
25
+ LINE_BREAK_SEGMENT_NAME,
26
+ PALETTE_NAMES,
27
+ PALETTE_PRESET_NAMES,
28
+ type PaletteName,
29
+ type PalettePreset,
30
+ SEGMENT_NAMES,
31
+ type SegmentName,
39
32
  } from "./types.js";
40
33
 
41
34
  const EDIT_SETTINGS_LABEL = "Edit settings JSON";
42
35
  const SEGMENT_VIEWPORT_SIZE = 6;
43
36
  const NARROW_SEGMENT_VIEWPORT_SIZE = 3;
44
37
  const SEGMENT_DESCRIPTIONS: Record<SegmentName, string> = {
45
- brand: "Pi brand mark",
46
- provider: "Current model provider",
47
- model: "Current model name",
48
- thinking: "Current thinking level",
49
- cwd: "Current working directory",
50
- branch: "Git branch, status, and linked pull request",
51
- tools: "Current tool and streaming activity",
52
- context: "Current context-window usage",
53
- tokens: "Session token totals",
54
- cache: "Prompt-cache reads, writes, and latest hit rate",
55
- cost: "Session cost",
56
- time: "Current local time",
57
- turn: "Current session turn count",
38
+ brand: "Pi brand mark",
39
+ provider: "Current model provider",
40
+ model: "Current model name",
41
+ thinking: "Current thinking level",
42
+ cwd: "Current working directory",
43
+ branch: "Git branch, status, and linked pull request",
44
+ tools: "Current tool and streaming activity",
45
+ context: "Current context-window usage",
46
+ tokens: "Session token totals",
47
+ cache: "Prompt-cache reads, writes, and latest hit rate",
48
+ cost: "Session cost",
49
+ time: "Current local time",
50
+ turn: "Current session turn count",
58
51
  };
59
52
  interface StatuslineMenuOwner {
60
- signal: AbortSignal;
61
- isCurrent(): boolean;
53
+ signal: AbortSignal;
54
+ isCurrent(): boolean;
62
55
  }
63
56
 
64
57
  export interface StatuslineCommandOptions {
65
- settingsPath: string;
66
- getLoaded(): LoadedStatuslineSettings;
67
- apply(loaded: LoadedStatuslineSettings, ctx: ExtensionCommandContext): void;
68
- preview?(palettePreset: PalettePreset | undefined, ctx: ExtensionCommandContext): void;
69
- save?: (settingsPath: string, rawDocument: string) => LoadedStatuslineSettings;
70
- getMenuOwner?(): StatuslineMenuOwner;
58
+ settingsPath: string;
59
+ getLoaded(): LoadedStatuslineSettings;
60
+ apply(loaded: LoadedStatuslineSettings, ctx: ExtensionCommandContext): void;
61
+ preview?(palettePreset: PalettePreset | undefined, ctx: ExtensionCommandContext): void;
62
+ save?: (settingsPath: string, rawDocument: string) => LoadedStatuslineSettings;
63
+ getMenuOwner?(): StatuslineMenuOwner;
71
64
  }
72
65
 
73
66
  export function registerStatuslineCommand(pi: ExtensionAPI, options: StatuslineCommandOptions) {
74
- pi.registerCommand("statusline", {
75
- description: "Open or inspect the statusline settings",
76
- getArgumentCompletions: completeStatuslineArguments,
77
- handler: (args, ctx) => handleStatuslineCommand(args, ctx, options),
78
- });
67
+ pi.registerCommand("statusline", {
68
+ description: "Open or inspect the statusline settings",
69
+ getArgumentCompletions: completeStatuslineArguments,
70
+ handler: (args, ctx) => handleStatuslineCommand(args, ctx, options),
71
+ });
79
72
  }
80
73
 
81
74
  export async function handleStatuslineCommand(
82
- args: string,
83
- ctx: ExtensionCommandContext,
84
- options: StatuslineCommandOptions,
75
+ args: string,
76
+ ctx: ExtensionCommandContext,
77
+ options: StatuslineCommandOptions,
85
78
  ) {
86
- const normalized = args.trim();
87
- if (!normalized) {
88
- await showMainMenu(ctx, options);
89
- return;
90
- }
91
-
92
- const tokens = normalized.split(/\s+/u);
93
- const subcommand = tokens[0]?.toLowerCase() ?? "";
94
- if (tokens.length > 1) {
95
- if (canNotify(ctx)) {
96
- ctx.ui.notify(`/statusline ${subcommand} does not accept trailing arguments.`, "warning");
97
- }
98
- return;
99
- }
100
-
101
- switch (subcommand) {
102
- case "settings":
103
- await editSettings(ctx, options);
104
- return;
105
- case "status":
106
- showStatus(ctx, options);
107
- return;
108
- case "help":
109
- showHelp(ctx, options.settingsPath);
110
- return;
111
- default:
112
- if (canNotify(ctx)) {
113
- ctx.ui.notify(`Unknown /statusline subcommand: ${subcommand}`, "warning");
114
- }
115
- }
79
+ const normalized = args.trim();
80
+ if (!normalized) {
81
+ await showMainMenu(ctx, options);
82
+ return;
83
+ }
84
+
85
+ const tokens = normalized.split(/\s+/u);
86
+ const subcommand = tokens[0]?.toLowerCase() ?? "";
87
+ if (tokens.length > 1) {
88
+ if (canNotify(ctx)) {
89
+ ctx.ui.notify(`/statusline ${subcommand} does not accept trailing arguments.`, "warning");
90
+ }
91
+ return;
92
+ }
93
+
94
+ switch (subcommand) {
95
+ case "settings":
96
+ await editSettings(ctx, options);
97
+ return;
98
+ case "status":
99
+ showStatus(ctx, options);
100
+ return;
101
+ case "help":
102
+ showHelp(ctx, options.settingsPath);
103
+ return;
104
+ default:
105
+ if (canNotify(ctx)) {
106
+ ctx.ui.notify(`Unknown /statusline subcommand: ${subcommand}`, "warning");
107
+ }
108
+ }
116
109
  }
117
110
 
118
111
  async function showMainMenu(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
119
- if (ctx.mode !== "tui") {
120
- if (ctx.hasUI) ctx.ui.notify("/statusline requires an interactive Pi UI in TUI mode.", "error");
121
- return;
122
- }
123
- const fallbackController = new AbortController();
124
- const owner = options.getMenuOwner?.() ?? {
125
- signal: fallbackController.signal,
126
- isCurrent: () => !fallbackController.signal.aborted,
127
- };
128
- const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
129
- if (owner.signal.aborted || !owner.isCurrent()) return;
130
- type Screen = "main" | "advanced" | "information";
131
- type Action = "appearance" | "setInformation" | "layout" | "edit" | "status" | "help" | "back";
132
- const menu = defineMenu<undefined, Screen, Action, ExtensionCommandContext>({
133
- start: "main",
134
- screens: {
135
- main: () => {
136
- const config = options.getLoaded().config;
137
- return {
138
- kind: "actions",
139
- title: "pi-statusline",
140
- items: [
141
- {
142
- id: "appearance",
143
- label: `Appearance (${config.palettePreset})`,
144
- action: "appearance",
145
- },
146
- {
147
- id: "information",
148
- label: `Information (${inferInformationProfile(config.segments)})`,
149
- to: "information",
150
- },
151
- { id: "advanced", label: "Advanced", to: "advanced" },
152
- { id: "status", label: "Status", action: "status" },
153
- { id: "help", label: "Help", action: "help" },
154
- ],
155
- hint: "close",
156
- };
157
- },
158
- information: () => {
159
- const current = inferInformationProfile(options.getLoaded().config.segments);
160
- return {
161
- kind: "choice",
162
- title: "Information level",
163
- lines: [`Current profile: ${current}`],
164
- items: INFORMATION_PROFILE_NAMES.map((profile) => ({
165
- id: profile,
166
- label: `${profile[0]?.toUpperCase() ?? ""}${profile.slice(1)}`,
167
- description: `${INFORMATION_PROFILES[profile].length} segments`,
168
- details: [`Segments: ${INFORMATION_PROFILES[profile].join(" · ")}`],
169
- })),
170
- action: "setInformation",
171
- currentItemId: current,
172
- initialItemId: current === "custom" ? "balanced" : current,
173
- hint: "close",
174
- };
175
- },
176
- advanced: () => {
177
- const config = options.getLoaded().config;
178
- const visibleSegmentCount = config.segments.filter(
179
- (segment): segment is SegmentName => segment !== LINE_BREAK_SEGMENT_NAME,
180
- ).length;
181
- return {
182
- kind: "actions",
183
- title: "pi-statusline — Advanced",
184
- items: [
185
- {
186
- id: "layout",
187
- label: `Custom layout (${visibleSegmentCount}/${SEGMENT_NAMES.length} shown)`,
188
- action: "layout",
189
- },
190
- { id: "edit", label: EDIT_SETTINGS_LABEL, action: "edit" },
191
- { id: "back", label: "Back", action: "back" },
192
- ],
193
- hint: "back",
194
- };
195
- },
196
- },
197
- actions: {
198
- appearance: async () => {
199
- await choosePalettePreset(ctx, options, owner);
200
- return { kind: "close" };
201
- },
202
- setInformation: async ({ itemId }) => {
203
- const profile = INFORMATION_PROFILE_NAMES.find((candidate) => candidate === itemId);
204
- if (!profile) return { kind: "rejected" };
205
- applyInformationProfile(ctx, options, profile);
206
- return { kind: "close" };
207
- },
208
- layout: async () => {
209
- await chooseSegments(ctx, options);
210
- return { kind: "close" };
211
- },
212
- edit: async () => {
213
- await editSettings(ctx, options);
214
- return { kind: "close" };
215
- },
216
- status: async () => {
217
- showStatus(ctx, options);
218
- return { kind: "close" };
219
- },
220
- help: async () => {
221
- showHelp(ctx, options.settingsPath);
222
- return { kind: "close" };
223
- },
224
- back: async () => ({ kind: "back" }),
225
- },
226
- });
227
- try {
228
- await runMenu(ctx, menu, {
229
- getState: () => undefined,
230
- signal: owner.signal,
231
- isCurrent: owner.isCurrent,
232
- });
233
- } finally {
234
- fallbackController.abort(new DOMException("Statusline menu closed", "AbortError"));
235
- }
112
+ if (ctx.mode !== "tui") {
113
+ if (ctx.hasUI) ctx.ui.notify("/statusline requires an interactive Pi UI in TUI mode.", "error");
114
+ return;
115
+ }
116
+ const fallbackController = new AbortController();
117
+ const owner = options.getMenuOwner?.() ?? {
118
+ signal: fallbackController.signal,
119
+ isCurrent: () => !fallbackController.signal.aborted,
120
+ };
121
+ const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
122
+ if (owner.signal.aborted || !owner.isCurrent()) return;
123
+ type Screen = "main" | "advanced" | "information";
124
+ type Action = "appearance" | "setInformation" | "layout" | "edit" | "status" | "help" | "back";
125
+ const menu = defineMenu<undefined, Screen, Action, ExtensionCommandContext>({
126
+ start: "main",
127
+ screens: {
128
+ main: () => {
129
+ const config = options.getLoaded().config;
130
+ return {
131
+ kind: "actions",
132
+ title: "pi-statusline",
133
+ items: [
134
+ {
135
+ id: "appearance",
136
+ label: `Appearance (${config.palettePreset})`,
137
+ action: "appearance",
138
+ },
139
+ {
140
+ id: "information",
141
+ label: `Information (${inferInformationProfile(config.segments)})`,
142
+ to: "information",
143
+ },
144
+ { id: "advanced", label: "Advanced", to: "advanced" },
145
+ { id: "status", label: "Status", action: "status" },
146
+ { id: "help", label: "Help", action: "help" },
147
+ ],
148
+ hint: "close",
149
+ };
150
+ },
151
+ information: () => {
152
+ const current = inferInformationProfile(options.getLoaded().config.segments);
153
+ return {
154
+ kind: "choice",
155
+ title: "Information level",
156
+ lines: [`Current profile: ${current}`],
157
+ items: INFORMATION_PROFILE_NAMES.map((profile) => ({
158
+ id: profile,
159
+ label: `${profile[0]?.toUpperCase() ?? ""}${profile.slice(1)}`,
160
+ description: `${INFORMATION_PROFILES[profile].length} segments`,
161
+ details: [`Segments: ${INFORMATION_PROFILES[profile].join(" · ")}`],
162
+ })),
163
+ action: "setInformation",
164
+ currentItemId: current,
165
+ initialItemId: current === "custom" ? "balanced" : current,
166
+ hint: "close",
167
+ };
168
+ },
169
+ advanced: () => {
170
+ const config = options.getLoaded().config;
171
+ const visibleSegmentCount = config.segments.filter(
172
+ (segment): segment is SegmentName => segment !== LINE_BREAK_SEGMENT_NAME,
173
+ ).length;
174
+ return {
175
+ kind: "actions",
176
+ title: "pi-statusline — Advanced",
177
+ items: [
178
+ {
179
+ id: "layout",
180
+ label: `Custom layout (${visibleSegmentCount}/${SEGMENT_NAMES.length} shown)`,
181
+ action: "layout",
182
+ },
183
+ { id: "edit", label: EDIT_SETTINGS_LABEL, action: "edit" },
184
+ { id: "back", label: "Back", action: "back" },
185
+ ],
186
+ hint: "back",
187
+ };
188
+ },
189
+ },
190
+ actions: {
191
+ appearance: async () => {
192
+ await choosePalettePreset(ctx, options, owner);
193
+ return { kind: "close" };
194
+ },
195
+ setInformation: async ({ itemId }) => {
196
+ const profile = INFORMATION_PROFILE_NAMES.find((candidate) => candidate === itemId);
197
+ if (!profile) return { kind: "rejected" };
198
+ applyInformationProfile(ctx, options, profile);
199
+ return { kind: "close" };
200
+ },
201
+ layout: async () => {
202
+ await chooseSegments(ctx, options);
203
+ return { kind: "close" };
204
+ },
205
+ edit: async () => {
206
+ await editSettings(ctx, options);
207
+ return { kind: "close" };
208
+ },
209
+ status: async () => {
210
+ showStatus(ctx, options);
211
+ return { kind: "close" };
212
+ },
213
+ help: async () => {
214
+ showHelp(ctx, options.settingsPath);
215
+ return { kind: "close" };
216
+ },
217
+ back: async () => ({ kind: "back" }),
218
+ },
219
+ });
220
+ try {
221
+ await runMenu(ctx, menu, {
222
+ getState: () => undefined,
223
+ signal: owner.signal,
224
+ isCurrent: owner.isCurrent,
225
+ });
226
+ } finally {
227
+ fallbackController.abort(new DOMException("Statusline menu closed", "AbortError"));
228
+ }
236
229
  }
237
230
 
238
231
  async function choosePalettePreset(
239
- ctx: ExtensionCommandContext,
240
- options: StatuslineCommandOptions,
241
- owner: StatuslineMenuOwner,
232
+ ctx: ExtensionCommandContext,
233
+ options: StatuslineCommandOptions,
234
+ owner: StatuslineMenuOwner,
242
235
  ) {
243
- if (ctx.mode !== "tui") {
244
- if (ctx.hasUI) ctx.ui.notify(`Edit palettePreset manually: ${options.settingsPath}`, "info");
245
- return;
246
- }
247
- const current = options.getLoaded();
248
- let selection: PalettePreset | undefined;
249
- try {
250
- selection = await showPalettePresetPicker(ctx, current.config.palettePreset, options, owner);
251
- } finally {
252
- if (!owner.signal.aborted && owner.isCurrent()) {
253
- try {
254
- options.preview?.(undefined, ctx);
255
- } catch (error) {
256
- if (canNotify(ctx)) {
257
- ctx.ui.notify(`Palette preview could not be reset: ${formatError(error)}`, "error");
258
- }
259
- }
260
- }
261
- }
262
- if (owner.signal.aborted || !owner.isCurrent()) return;
263
- if (selection === undefined) return;
264
-
265
- let loaded: LoadedStatuslineSettings;
266
- try {
267
- const rawDocument = palettePresetDocument(current, selection);
268
- loaded = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, rawDocument);
269
- } catch (error) {
270
- ctx.ui.notify(`Palette preset was not saved: ${formatError(error)}`, "error");
271
- return;
272
- }
273
- try {
274
- options.apply(loaded, ctx);
275
- } catch (error) {
276
- const rollbackError = restoreStatuslineSettings(ctx, options, current, loaded);
277
- ctx.ui.notify(
278
- rollbackError
279
- ? `Palette preset could not be applied: ${formatError(error)}; rollback failed: ${formatError(rollbackError)}`
280
- : `Palette preset could not be applied: ${formatError(error)}; previous settings restored.`,
281
- "error",
282
- );
283
- return;
284
- }
285
- const message =
286
- loaded.config.palettePreset === "custom"
287
- ? "Custom palette applied. Edit colors via /statusline → Advanced → Edit settings JSON."
288
- : `Palette preset applied: ${loaded.config.palettePreset}.`;
289
- ctx.ui.notify(message, "info");
236
+ if (ctx.mode !== "tui") {
237
+ if (ctx.hasUI) ctx.ui.notify(`Edit palettePreset manually: ${options.settingsPath}`, "info");
238
+ return;
239
+ }
240
+ const current = options.getLoaded();
241
+ let selection: PalettePreset | undefined;
242
+ try {
243
+ selection = await showPalettePresetPicker(ctx, current.config.palettePreset, options, owner);
244
+ } finally {
245
+ if (!owner.signal.aborted && owner.isCurrent()) {
246
+ try {
247
+ options.preview?.(undefined, ctx);
248
+ } catch (error) {
249
+ if (canNotify(ctx)) {
250
+ ctx.ui.notify(`Palette preview could not be reset: ${formatError(error)}`, "error");
251
+ }
252
+ }
253
+ }
254
+ }
255
+ if (owner.signal.aborted || !owner.isCurrent()) return;
256
+ if (selection === undefined) return;
257
+
258
+ let loaded: LoadedStatuslineSettings;
259
+ try {
260
+ const rawDocument = palettePresetDocument(current, selection);
261
+ loaded = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, rawDocument);
262
+ } catch (error) {
263
+ ctx.ui.notify(`Palette preset was not saved: ${formatError(error)}`, "error");
264
+ return;
265
+ }
266
+ try {
267
+ options.apply(loaded, ctx);
268
+ } catch (error) {
269
+ const rollbackError = restoreStatuslineSettings(ctx, options, current, loaded);
270
+ ctx.ui.notify(
271
+ rollbackError
272
+ ? `Palette preset could not be applied: ${formatError(error)}; rollback failed: ${formatError(rollbackError)}`
273
+ : `Palette preset could not be applied: ${formatError(error)}; previous settings restored.`,
274
+ "error",
275
+ );
276
+ return;
277
+ }
278
+ const message =
279
+ loaded.config.palettePreset === "custom"
280
+ ? "Custom palette applied. Edit colors via /statusline → Advanced → Edit settings JSON."
281
+ : `Palette preset applied: ${loaded.config.palettePreset}.`;
282
+ ctx.ui.notify(message, "info");
290
283
  }
291
284
 
292
285
  async function showPalettePresetPicker(
293
- ctx: ExtensionCommandContext,
294
- current: PalettePreset,
295
- options: StatuslineCommandOptions,
296
- owner: StatuslineMenuOwner,
286
+ ctx: ExtensionCommandContext,
287
+ current: PalettePreset,
288
+ options: StatuslineCommandOptions,
289
+ owner: StatuslineMenuOwner,
297
290
  ): Promise<PalettePreset | undefined> {
298
- const { runLiveChoice } = await import("@narumitw/pi-tui-kit");
299
- const result = await runLiveChoice(ctx, {
300
- title: `Palette preset (current: ${current})`,
301
- items: PALETTE_PRESET_NAMES.map((palettePreset) => ({
302
- id: palettePreset,
303
- label: palettePreset,
304
- description: palettePreset === "custom" ? "per-segment colors from settings JSON" : undefined,
305
- })),
306
- currentItemId: current,
307
- initialItemId: current,
308
- viewportSize: Math.min(PALETTE_PRESET_NAMES.length, 10),
309
- hint: "close",
310
- navigationLabel: "preview",
311
- confirmLabel: "apply",
312
- onSelectionChange: ({ item }) => options.preview?.(item.id, ctx),
313
- signal: owner.signal,
314
- isCurrent: owner.isCurrent,
315
- });
316
- return result.kind === "selected" ? result.itemId : undefined;
291
+ const { runLiveChoice } = await import("@narumitw/pi-tui-kit");
292
+ const result = await runLiveChoice(ctx, {
293
+ title: `Palette preset (current: ${current})`,
294
+ items: PALETTE_PRESET_NAMES.map((palettePreset) => ({
295
+ id: palettePreset,
296
+ label: palettePreset,
297
+ description: palettePreset === "custom" ? "per-segment colors from settings JSON" : undefined,
298
+ })),
299
+ currentItemId: current,
300
+ initialItemId: current,
301
+ viewportSize: Math.min(PALETTE_PRESET_NAMES.length, 10),
302
+ hint: "close",
303
+ navigationLabel: "preview",
304
+ confirmLabel: "apply",
305
+ onSelectionChange: ({ item }) => options.preview?.(item.id, ctx),
306
+ signal: owner.signal,
307
+ isCurrent: owner.isCurrent,
308
+ });
309
+ return result.kind === "selected" ? result.itemId : undefined;
317
310
  }
318
311
 
319
312
  function applyInformationProfile(
320
- ctx: ExtensionCommandContext,
321
- options: StatuslineCommandOptions,
322
- selection: InformationProfileName,
313
+ ctx: ExtensionCommandContext,
314
+ options: StatuslineCommandOptions,
315
+ selection: InformationProfileName,
323
316
  ) {
324
- const current = options.getLoaded();
325
- try {
326
- const change = informationProfileDocument(current, selection);
327
- const loaded = applySegmentsDocumentChange(change, ctx, options);
328
- ctx.ui.notify(
329
- `Information level applied: ${inferInformationProfile(loaded.config.segments)}.`,
330
- "info",
331
- );
332
- } catch (error) {
333
- ctx.ui.notify(`Information level was not saved: ${formatError(error)}`, "error");
334
- }
317
+ const current = options.getLoaded();
318
+ try {
319
+ const change = informationProfileDocument(current, selection);
320
+ const loaded = applySegmentsDocumentChange(change, ctx, options);
321
+ ctx.ui.notify(`Information level applied: ${inferInformationProfile(loaded.config.segments)}.`, "info");
322
+ } catch (error) {
323
+ ctx.ui.notify(`Information level was not saved: ${formatError(error)}`, "error");
324
+ }
335
325
  }
336
326
 
337
327
  async function chooseSegments(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
338
- if (ctx.mode !== "tui") {
339
- if (ctx.hasUI) ctx.ui.notify(`Edit segments manually: ${options.settingsPath}`, "info");
340
- return;
341
- }
342
- let current = options.getLoaded();
343
- let names = segmentMenuOrder(current);
344
- let selectedIndex = 0;
345
- let moveMode = false;
346
- let feedback: string | undefined;
347
-
348
- await ctx.ui.custom((tui, theme, keybindings, done) => {
349
- const commit = (
350
- name: SegmentName,
351
- change: { nextDocument: string; previousDocument: string },
352
- ): boolean => {
353
- try {
354
- current = applySegmentsDocumentChange(change, ctx, options);
355
- names = segmentMenuOrder(current);
356
- selectedIndex = Math.max(0, names.indexOf(name));
357
- feedback = undefined;
358
- return true;
359
- } catch (error) {
360
- ctx.ui.notify(`Statusline segments were not saved: ${formatError(error)}`, "error");
361
- return false;
362
- }
363
- };
364
-
365
- const toggleSelected = () => {
366
- const name = names[selectedIndex];
367
- if (!name) return;
368
- moveMode = false;
369
- commit(name, segmentsDocument(current, name, !current.config.segments.includes(name)));
370
- };
371
-
372
- const toggleLineBreakAfterSelected = () => {
373
- const name = names[selectedIndex];
374
- if (!name) return;
375
- moveMode = false;
376
- const segmentIndex = current.config.segments.indexOf(name);
377
- if (segmentIndex < 0) {
378
- feedback = `Show ${name} before adding a line break.`;
379
- return;
380
- }
381
- const hasLineBreak = current.config.segments[segmentIndex + 1] === LINE_BREAK_SEGMENT_NAME;
382
- const hasFollowingSegment = current.config.segments
383
- .slice(segmentIndex + 1)
384
- .some((segment) => segment !== LINE_BREAK_SEGMENT_NAME);
385
- if (!hasLineBreak && !hasFollowingSegment) {
386
- feedback = `Add another visible segment after ${name} before adding a line break.`;
387
- return;
388
- }
389
- commit(name, lineBreakAfterDocument(current, name));
390
- };
391
-
392
- const reorderSelected = (direction: -1 | 1) => {
393
- const name = names[selectedIndex];
394
- if (!name) return;
395
- const visibleNames = visibleSegmentNames(current);
396
- const currentIndex = visibleNames.indexOf(name);
397
- if (currentIndex < 0) {
398
- feedback = `Show ${name} before moving it.`;
399
- return;
400
- }
401
- const targetIndex = currentIndex + direction;
402
- if (targetIndex < 0 || targetIndex >= visibleNames.length) {
403
- feedback = `${name} is already the ${direction < 0 ? "first" : "last"} visible segment.`;
404
- return;
405
- }
406
- const change = reorderedSegmentsDocument(current, name, direction);
407
- if (change) commit(name, change);
408
- };
409
-
410
- const enterMoveMode = () => {
411
- const name = names[selectedIndex];
412
- if (!name) return;
413
- if (!current.config.segments.includes(name)) {
414
- feedback = `Show ${name} before moving it.`;
415
- return;
416
- }
417
- moveMode = true;
418
- feedback = undefined;
419
- };
420
-
421
- const moveSelection = (offset: number) => {
422
- selectedIndex = Math.max(0, Math.min(names.length - 1, selectedIndex + offset));
423
- feedback = undefined;
424
- };
425
-
426
- const renderList = (width: number): string[] => {
427
- const safeWidth = Math.max(1, width);
428
- const visibleNames = visibleSegmentNames(current);
429
- const visible = new Set(visibleNames);
430
- const placements = segmentPlacements(current.config.segments);
431
- const viewportSize = safeWidth < 30 ? NARROW_SEGMENT_VIEWPORT_SIZE : SEGMENT_VIEWPORT_SIZE;
432
- const startIndex = Math.max(
433
- 0,
434
- Math.min(
435
- selectedIndex - Math.floor(viewportSize / 2),
436
- Math.max(0, names.length - viewportSize),
437
- ),
438
- );
439
- const endIndex = Math.min(names.length, startIndex + viewportSize);
440
- const lines: string[] = [];
441
- let previousSection: "visible" | "hidden" | undefined;
442
- for (let index = startIndex; index < endIndex; index += 1) {
443
- const name = names[index];
444
- if (!name) continue;
445
- const section = visible.has(name) ? "visible" : "hidden";
446
- if (section !== previousSection) {
447
- const heading =
448
- section === "visible" ? "Visible — render order" : "Hidden — not rendered";
449
- lines.push(theme.fg("muted", theme.bold(truncateToWidth(` ${heading}`, safeWidth))));
450
- previousSection = section;
451
- }
452
- const isSelected = index === selectedIndex;
453
- const prefix = isSelected ? "→ " : " ";
454
- const placement = placements.get(name);
455
- const lineBreakLabel = hasLineBreakAfter(current.config.segments, name)
456
- ? " · break after"
457
- : "";
458
- const row = placement
459
- ? `${prefix}${`${placement.order}.`.padStart(3)} row ${placement.row} · ${name.padEnd(8)} · visible${lineBreakLabel}`
460
- : `${prefix} · ${name.padEnd(8)} · hidden`;
461
- const truncated = truncateToWidth(row, safeWidth);
462
- if (isSelected) lines.push(theme.fg("accent", truncated));
463
- else lines.push(section === "visible" ? truncated : theme.fg("muted", truncated));
464
- }
465
- if (startIndex > 0 || endIndex < names.length) {
466
- lines.push(
467
- theme.fg(
468
- "dim",
469
- truncateToWidth(` (${startIndex + 1}-${endIndex}/${names.length})`, safeWidth),
470
- ),
471
- );
472
- }
473
-
474
- const selected = names[selectedIndex];
475
- if (selected) {
476
- lines.push("");
477
- const detail = feedback ?? SEGMENT_DESCRIPTIONS[selected];
478
- const color = feedback ? "warning" : "muted";
479
- for (const line of wrapTextWithAnsi(detail, Math.max(1, safeWidth - 2))) {
480
- lines.push(theme.fg(color, truncateToWidth(` ${line}`, safeWidth)));
481
- }
482
- }
483
-
484
- lines.push("");
485
- const hints = segmentControlHints(safeWidth, moveMode, selected, keybindings);
486
- for (const hint of hints) {
487
- lines.push(theme.fg("dim", truncateToWidth(` ${hint}`, safeWidth)));
488
- }
489
- return lines;
490
- };
491
-
492
- const container = new Container();
493
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
494
- const title = new Text("", 1, 0);
495
- container.addChild(title);
496
- container.addChild({ render: renderList, invalidate() {} });
497
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
498
- const updateThemedText = () => {
499
- title.setText(theme.fg("accent", theme.bold("Statusline segments")));
500
- };
501
- updateThemedText();
502
-
503
- return {
504
- render: (width: number) => container.render(width),
505
- invalidate() {
506
- container.invalidate();
507
- updateThemedText();
508
- },
509
- handleInput(data: string) {
510
- if (matchesKey(data, Key.ctrl("c"))) {
511
- done(undefined);
512
- return;
513
- }
514
- const moveModeKey = matchesKey(data, "m") || matchesKey(data, Key.shift("m"));
515
- const lineBreakKey = matchesKey(data, "b") || matchesKey(data, Key.shift("b"));
516
- if (matchesKey(data, Key.alt("up"))) {
517
- reorderSelected(-1);
518
- } else if (matchesKey(data, Key.alt("down"))) {
519
- reorderSelected(1);
520
- } else if (moveMode) {
521
- if (keybindings.matches(data, "tui.select.up")) {
522
- reorderSelected(-1);
523
- } else if (keybindings.matches(data, "tui.select.down")) {
524
- reorderSelected(1);
525
- } else if (
526
- moveModeKey ||
527
- keybindings.matches(data, "tui.select.confirm") ||
528
- data === " " ||
529
- keybindings.matches(data, "tui.select.cancel")
530
- ) {
531
- moveMode = false;
532
- feedback = undefined;
533
- }
534
- } else if (keybindings.matches(data, "tui.select.up")) {
535
- selectedIndex = selectedIndex === 0 ? names.length - 1 : selectedIndex - 1;
536
- feedback = undefined;
537
- } else if (keybindings.matches(data, "tui.select.down")) {
538
- selectedIndex = selectedIndex === names.length - 1 ? 0 : selectedIndex + 1;
539
- feedback = undefined;
540
- } else if (keybindings.matches(data, "tui.select.pageUp")) {
541
- moveSelection(-SEGMENT_VIEWPORT_SIZE);
542
- } else if (keybindings.matches(data, "tui.select.pageDown")) {
543
- moveSelection(SEGMENT_VIEWPORT_SIZE);
544
- } else if (moveModeKey) {
545
- enterMoveMode();
546
- } else if (lineBreakKey) {
547
- toggleLineBreakAfterSelected();
548
- } else if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
549
- toggleSelected();
550
- } else if (keybindings.matches(data, "tui.select.cancel")) {
551
- done(undefined);
552
- }
553
- tui.requestRender();
554
- },
555
- };
556
- });
328
+ if (ctx.mode !== "tui") {
329
+ if (ctx.hasUI) ctx.ui.notify(`Edit segments manually: ${options.settingsPath}`, "info");
330
+ return;
331
+ }
332
+ let current = options.getLoaded();
333
+ let names = segmentMenuOrder(current);
334
+ let selectedIndex = 0;
335
+ let moveMode = false;
336
+ let feedback: string | undefined;
337
+
338
+ await ctx.ui.custom((tui, theme, keybindings, done) => {
339
+ const commit = (name: SegmentName, change: { nextDocument: string; previousDocument: string }): boolean => {
340
+ try {
341
+ current = applySegmentsDocumentChange(change, ctx, options);
342
+ names = segmentMenuOrder(current);
343
+ selectedIndex = Math.max(0, names.indexOf(name));
344
+ feedback = undefined;
345
+ return true;
346
+ } catch (error) {
347
+ ctx.ui.notify(`Statusline segments were not saved: ${formatError(error)}`, "error");
348
+ return false;
349
+ }
350
+ };
351
+
352
+ const toggleSelected = () => {
353
+ const name = names[selectedIndex];
354
+ if (!name) return;
355
+ moveMode = false;
356
+ commit(name, segmentsDocument(current, name, !current.config.segments.includes(name)));
357
+ };
358
+
359
+ const toggleLineBreakAfterSelected = () => {
360
+ const name = names[selectedIndex];
361
+ if (!name) return;
362
+ moveMode = false;
363
+ const segmentIndex = current.config.segments.indexOf(name);
364
+ if (segmentIndex < 0) {
365
+ feedback = `Show ${name} before adding a line break.`;
366
+ return;
367
+ }
368
+ const hasLineBreak = current.config.segments[segmentIndex + 1] === LINE_BREAK_SEGMENT_NAME;
369
+ const hasFollowingSegment = current.config.segments
370
+ .slice(segmentIndex + 1)
371
+ .some((segment) => segment !== LINE_BREAK_SEGMENT_NAME);
372
+ if (!hasLineBreak && !hasFollowingSegment) {
373
+ feedback = `Add another visible segment after ${name} before adding a line break.`;
374
+ return;
375
+ }
376
+ commit(name, lineBreakAfterDocument(current, name));
377
+ };
378
+
379
+ const reorderSelected = (direction: -1 | 1) => {
380
+ const name = names[selectedIndex];
381
+ if (!name) return;
382
+ const visibleNames = visibleSegmentNames(current);
383
+ const currentIndex = visibleNames.indexOf(name);
384
+ if (currentIndex < 0) {
385
+ feedback = `Show ${name} before moving it.`;
386
+ return;
387
+ }
388
+ const targetIndex = currentIndex + direction;
389
+ if (targetIndex < 0 || targetIndex >= visibleNames.length) {
390
+ feedback = `${name} is already the ${direction < 0 ? "first" : "last"} visible segment.`;
391
+ return;
392
+ }
393
+ const change = reorderedSegmentsDocument(current, name, direction);
394
+ if (change) commit(name, change);
395
+ };
396
+
397
+ const enterMoveMode = () => {
398
+ const name = names[selectedIndex];
399
+ if (!name) return;
400
+ if (!current.config.segments.includes(name)) {
401
+ feedback = `Show ${name} before moving it.`;
402
+ return;
403
+ }
404
+ moveMode = true;
405
+ feedback = undefined;
406
+ };
407
+
408
+ const moveSelection = (offset: number) => {
409
+ selectedIndex = Math.max(0, Math.min(names.length - 1, selectedIndex + offset));
410
+ feedback = undefined;
411
+ };
412
+
413
+ const renderList = (width: number): string[] => {
414
+ const safeWidth = Math.max(1, width);
415
+ const visibleNames = visibleSegmentNames(current);
416
+ const visible = new Set(visibleNames);
417
+ const placements = segmentPlacements(current.config.segments);
418
+ const viewportSize = safeWidth < 30 ? NARROW_SEGMENT_VIEWPORT_SIZE : SEGMENT_VIEWPORT_SIZE;
419
+ const startIndex = Math.max(
420
+ 0,
421
+ Math.min(selectedIndex - Math.floor(viewportSize / 2), Math.max(0, names.length - viewportSize)),
422
+ );
423
+ const endIndex = Math.min(names.length, startIndex + viewportSize);
424
+ const lines: string[] = [];
425
+ let previousSection: "visible" | "hidden" | undefined;
426
+ for (let index = startIndex; index < endIndex; index += 1) {
427
+ const name = names[index];
428
+ if (!name) continue;
429
+ const section = visible.has(name) ? "visible" : "hidden";
430
+ if (section !== previousSection) {
431
+ const heading = section === "visible" ? "Visible — render order" : "Hidden — not rendered";
432
+ lines.push(theme.fg("muted", theme.bold(truncateToWidth(` ${heading}`, safeWidth))));
433
+ previousSection = section;
434
+ }
435
+ const isSelected = index === selectedIndex;
436
+ const prefix = isSelected ? "→ " : " ";
437
+ const placement = placements.get(name);
438
+ const lineBreakLabel = hasLineBreakAfter(current.config.segments, name) ? " · break after" : "";
439
+ const row = placement
440
+ ? `${prefix}${`${placement.order}.`.padStart(3)} row ${placement.row} · ${name.padEnd(8)} · visible${lineBreakLabel}`
441
+ : `${prefix} · ${name.padEnd(8)} · hidden`;
442
+ const truncated = truncateToWidth(row, safeWidth);
443
+ if (isSelected) lines.push(theme.fg("accent", truncated));
444
+ else lines.push(section === "visible" ? truncated : theme.fg("muted", truncated));
445
+ }
446
+ if (startIndex > 0 || endIndex < names.length) {
447
+ lines.push(theme.fg("dim", truncateToWidth(` (${startIndex + 1}-${endIndex}/${names.length})`, safeWidth)));
448
+ }
449
+
450
+ const selected = names[selectedIndex];
451
+ if (selected) {
452
+ lines.push("");
453
+ const detail = feedback ?? SEGMENT_DESCRIPTIONS[selected];
454
+ const color = feedback ? "warning" : "muted";
455
+ for (const line of wrapTextWithAnsi(detail, Math.max(1, safeWidth - 2))) {
456
+ lines.push(theme.fg(color, truncateToWidth(` ${line}`, safeWidth)));
457
+ }
458
+ }
459
+
460
+ lines.push("");
461
+ const hints = segmentControlHints(safeWidth, moveMode, selected, keybindings);
462
+ for (const hint of hints) {
463
+ lines.push(theme.fg("dim", truncateToWidth(` ${hint}`, safeWidth)));
464
+ }
465
+ return lines;
466
+ };
467
+
468
+ const container = new Container();
469
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
470
+ const title = new Text("", 1, 0);
471
+ container.addChild(title);
472
+ container.addChild({ render: renderList, invalidate() {} });
473
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
474
+ const updateThemedText = () => {
475
+ title.setText(theme.fg("accent", theme.bold("Statusline segments")));
476
+ };
477
+ updateThemedText();
478
+
479
+ return {
480
+ render: (width: number) => container.render(width),
481
+ invalidate() {
482
+ container.invalidate();
483
+ updateThemedText();
484
+ },
485
+ handleInput(data: string) {
486
+ if (matchesKey(data, Key.ctrl("c"))) {
487
+ done(undefined);
488
+ return;
489
+ }
490
+ const moveModeKey = matchesKey(data, "m") || matchesKey(data, Key.shift("m"));
491
+ const lineBreakKey = matchesKey(data, "b") || matchesKey(data, Key.shift("b"));
492
+ if (matchesKey(data, Key.alt("up"))) {
493
+ reorderSelected(-1);
494
+ } else if (matchesKey(data, Key.alt("down"))) {
495
+ reorderSelected(1);
496
+ } else if (moveMode) {
497
+ if (keybindings.matches(data, "tui.select.up")) {
498
+ reorderSelected(-1);
499
+ } else if (keybindings.matches(data, "tui.select.down")) {
500
+ reorderSelected(1);
501
+ } else if (
502
+ moveModeKey ||
503
+ keybindings.matches(data, "tui.select.confirm") ||
504
+ data === " " ||
505
+ keybindings.matches(data, "tui.select.cancel")
506
+ ) {
507
+ moveMode = false;
508
+ feedback = undefined;
509
+ }
510
+ } else if (keybindings.matches(data, "tui.select.up")) {
511
+ selectedIndex = selectedIndex === 0 ? names.length - 1 : selectedIndex - 1;
512
+ feedback = undefined;
513
+ } else if (keybindings.matches(data, "tui.select.down")) {
514
+ selectedIndex = selectedIndex === names.length - 1 ? 0 : selectedIndex + 1;
515
+ feedback = undefined;
516
+ } else if (keybindings.matches(data, "tui.select.pageUp")) {
517
+ moveSelection(-SEGMENT_VIEWPORT_SIZE);
518
+ } else if (keybindings.matches(data, "tui.select.pageDown")) {
519
+ moveSelection(SEGMENT_VIEWPORT_SIZE);
520
+ } else if (moveModeKey) {
521
+ enterMoveMode();
522
+ } else if (lineBreakKey) {
523
+ toggleLineBreakAfterSelected();
524
+ } else if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
525
+ toggleSelected();
526
+ } else if (keybindings.matches(data, "tui.select.cancel")) {
527
+ done(undefined);
528
+ }
529
+ tui.requestRender();
530
+ },
531
+ };
532
+ });
557
533
  }
558
534
 
559
535
  async function editSettings(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
560
- if (ctx.mode !== "tui") {
561
- if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${options.settingsPath}`, "info");
562
- return;
563
- }
564
- const current = options.getLoaded();
565
- const edited = await ctx.ui.editor(
566
- "pi-statusline.json — save and close to apply",
567
- current.rawDocument ?? DEFAULT_STATUSLINE_DOCUMENT,
568
- );
569
- if (edited === undefined) return;
570
- let loaded: LoadedStatuslineSettings;
571
- try {
572
- loaded = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, edited);
573
- } catch (error) {
574
- ctx.ui.notify(`pi-statusline settings were not saved: ${formatError(error)}`, "error");
575
- return;
576
- }
577
- try {
578
- options.apply(loaded, ctx);
579
- } catch (error) {
580
- const rollbackError = restoreStatuslineSettings(ctx, options, current, loaded);
581
- ctx.ui.notify(
582
- rollbackError
583
- ? `pi-statusline settings could not be applied: ${formatError(error)}; rollback failed: ${formatError(rollbackError)}`
584
- : `pi-statusline settings could not be applied: ${formatError(error)}; previous settings restored.`,
585
- "error",
586
- );
587
- return;
588
- }
589
- const suffix =
590
- loaded.diagnostics.length > 0
591
- ? ` (${loaded.diagnostics.length} warning${loaded.diagnostics.length === 1 ? "" : "s"})`
592
- : "";
593
- ctx.ui.notify(`pi-statusline settings saved and applied${suffix}.`, "info");
536
+ if (ctx.mode !== "tui") {
537
+ if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${options.settingsPath}`, "info");
538
+ return;
539
+ }
540
+ const current = options.getLoaded();
541
+ const edited = await ctx.ui.editor(
542
+ "pi-statusline.json — save and close to apply",
543
+ current.rawDocument ?? DEFAULT_STATUSLINE_DOCUMENT,
544
+ );
545
+ if (edited === undefined) return;
546
+ let loaded: LoadedStatuslineSettings;
547
+ try {
548
+ loaded = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, edited);
549
+ } catch (error) {
550
+ ctx.ui.notify(`pi-statusline settings were not saved: ${formatError(error)}`, "error");
551
+ return;
552
+ }
553
+ try {
554
+ options.apply(loaded, ctx);
555
+ } catch (error) {
556
+ const rollbackError = restoreStatuslineSettings(ctx, options, current, loaded);
557
+ ctx.ui.notify(
558
+ rollbackError
559
+ ? `pi-statusline settings could not be applied: ${formatError(error)}; rollback failed: ${formatError(rollbackError)}`
560
+ : `pi-statusline settings could not be applied: ${formatError(error)}; previous settings restored.`,
561
+ "error",
562
+ );
563
+ return;
564
+ }
565
+ const suffix =
566
+ loaded.diagnostics.length > 0
567
+ ? ` (${loaded.diagnostics.length} warning${loaded.diagnostics.length === 1 ? "" : "s"})`
568
+ : "";
569
+ ctx.ui.notify(`pi-statusline settings saved and applied${suffix}.`, "info");
594
570
  }
595
571
 
596
- function palettePresetDocument(
597
- current: LoadedStatuslineSettings,
598
- palettePreset: PalettePreset,
599
- ): string {
600
- const { parsed } = editableSettings(current, "choosing a palette preset");
601
- if (palettePreset === "custom" && !isRecord(parsed.palette)) {
602
- const seedPreset = isPaletteName(current.config.palettePreset)
603
- ? current.config.palettePreset
604
- : "tokyo-night";
605
- parsed.palette = segmentPaletteForPreset(seedPreset);
606
- } else if (palettePreset !== "custom" && typeof parsed.palette === "string") {
607
- delete parsed.palette;
608
- }
609
- parsed.palettePreset = palettePreset;
610
- return `${JSON.stringify(parsed, null, "\t")}\n`;
572
+ function palettePresetDocument(current: LoadedStatuslineSettings, palettePreset: PalettePreset): string {
573
+ const { parsed } = editableSettings(current, "choosing a palette preset");
574
+ if (palettePreset === "custom" && !isRecord(parsed.palette)) {
575
+ const seedPreset = isPaletteName(current.config.palettePreset) ? current.config.palettePreset : "tokyo-night";
576
+ parsed.palette = segmentPaletteForPreset(seedPreset);
577
+ } else if (palettePreset !== "custom" && typeof parsed.palette === "string") {
578
+ delete parsed.palette;
579
+ }
580
+ parsed.palettePreset = palettePreset;
581
+ return `${JSON.stringify(parsed, null, "\t")}\n`;
611
582
  }
612
583
 
613
584
  function informationProfileDocument(
614
- current: LoadedStatuslineSettings,
615
- profile: InformationProfileName,
585
+ current: LoadedStatuslineSettings,
586
+ profile: InformationProfileName,
616
587
  ): { nextDocument: string; previousDocument: string } {
617
- const { parsed, rawDocument: previousDocument } = editableSettings(
618
- current,
619
- "changing information level",
620
- );
621
- parsed.segments = [...INFORMATION_PROFILES[profile]];
622
- return {
623
- nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
624
- previousDocument,
625
- };
588
+ const { parsed, rawDocument: previousDocument } = editableSettings(current, "changing information level");
589
+ parsed.segments = [...INFORMATION_PROFILES[profile]];
590
+ return {
591
+ nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
592
+ previousDocument,
593
+ };
626
594
  }
627
595
 
628
596
  function visibleSegmentNames(current: LoadedStatuslineSettings): SegmentName[] {
629
- return current.config.segments.filter(
630
- (segment): segment is SegmentName => segment !== LINE_BREAK_SEGMENT_NAME,
631
- );
597
+ return current.config.segments.filter((segment): segment is SegmentName => segment !== LINE_BREAK_SEGMENT_NAME);
632
598
  }
633
599
 
634
600
  function segmentMenuOrder(current: LoadedStatuslineSettings): SegmentName[] {
635
- const visible = visibleSegmentNames(current);
636
- const visibleSet = new Set(visible);
637
- return [...visible, ...SEGMENT_NAMES.filter((name) => !visibleSet.has(name))];
601
+ const visible = visibleSegmentNames(current);
602
+ const visibleSet = new Set(visible);
603
+ return [...visible, ...SEGMENT_NAMES.filter((name) => !visibleSet.has(name))];
638
604
  }
639
605
 
640
606
  function hasLineBreakAfter(segments: readonly ConfigSegmentName[], name: SegmentName): boolean {
641
- const index = segments.indexOf(name);
642
- return index >= 0 && segments[index + 1] === LINE_BREAK_SEGMENT_NAME;
607
+ const index = segments.indexOf(name);
608
+ return index >= 0 && segments[index + 1] === LINE_BREAK_SEGMENT_NAME;
643
609
  }
644
610
 
645
- function segmentPlacements(
646
- segments: readonly ConfigSegmentName[],
647
- ): Map<SegmentName, { order: number; row: number }> {
648
- const placements = new Map<SegmentName, { order: number; row: number }>();
649
- let order = 0;
650
- let row = 1;
651
- for (const segment of segments) {
652
- if (segment === LINE_BREAK_SEGMENT_NAME) {
653
- row += 1;
654
- continue;
655
- }
656
- placements.set(segment, { order: ++order, row });
657
- }
658
- return placements;
611
+ function segmentPlacements(segments: readonly ConfigSegmentName[]): Map<SegmentName, { order: number; row: number }> {
612
+ const placements = new Map<SegmentName, { order: number; row: number }>();
613
+ let order = 0;
614
+ let row = 1;
615
+ for (const segment of segments) {
616
+ if (segment === LINE_BREAK_SEGMENT_NAME) {
617
+ row += 1;
618
+ continue;
619
+ }
620
+ placements.set(segment, { order: ++order, row });
621
+ }
622
+ return placements;
659
623
  }
660
624
 
661
625
  function segmentControlHints(
662
- width: number,
663
- moveMode: boolean,
664
- selected: SegmentName | undefined,
665
- keybindings: Pick<KeybindingsManager, "getKeys">,
626
+ width: number,
627
+ moveMode: boolean,
628
+ selected: SegmentName | undefined,
629
+ keybindings: Pick<KeybindingsManager, "getKeys">,
666
630
  ): string[] {
667
- const configuredCancel = [
668
- ...new Set(
669
- keybindings
670
- .getKeys("tui.select.cancel")
671
- .filter((key) => key !== "ctrl+c")
672
- .map(displayKey)
673
- .filter(Boolean),
674
- ),
675
- ];
676
- const closeKeys = [...new Set([...configuredCancel, "Ctrl+C"])].join("/");
677
- if (moveMode) {
678
- const cancellation = [
679
- ...(configuredCancel.length > 0 ? [`${configuredCancel.join("/")} leave move mode`] : []),
680
- "Ctrl+C close",
681
- ].join(" · ");
682
- return width < 30
683
- ? [
684
- `Move mode: ${selected ?? "segment"}`,
685
- "↑↓ move segment",
686
- "Enter/Space finish",
687
- ...(configuredCancel.length > 0 ? [`${configuredCancel.join("/")} leave move mode`] : []),
688
- "Ctrl+C close",
689
- ]
690
- : [`Move mode — ${selected ?? "segment"}`, "↑↓ move · Enter/Space finish", cancellation];
691
- }
692
- return width < 30
693
- ? [
694
- "↑↓ navigate",
695
- "Enter/Space toggle",
696
- "M move · B line break",
697
- "Alt+↑/↓ quick move",
698
- `${closeKeys} close`,
699
- ]
700
- : [
701
- "↑↓ navigate · Enter/Space show/hide · M move mode",
702
- `B add/remove line break after · Alt+↑/↓ quick move · ${closeKeys} close`,
703
- ];
631
+ const configuredCancel = [
632
+ ...new Set(
633
+ keybindings
634
+ .getKeys("tui.select.cancel")
635
+ .filter((key) => key !== "ctrl+c")
636
+ .map(displayKey)
637
+ .filter(Boolean),
638
+ ),
639
+ ];
640
+ const closeKeys = [...new Set([...configuredCancel, "Ctrl+C"])].join("/");
641
+ if (moveMode) {
642
+ const cancellation = [
643
+ ...(configuredCancel.length > 0 ? [`${configuredCancel.join("/")} leave move mode`] : []),
644
+ "Ctrl+C close",
645
+ ].join(" · ");
646
+ return width < 30
647
+ ? [
648
+ `Move mode: ${selected ?? "segment"}`,
649
+ "↑↓ move segment",
650
+ "Enter/Space finish",
651
+ ...(configuredCancel.length > 0 ? [`${configuredCancel.join("/")} leave move mode`] : []),
652
+ "Ctrl+C close",
653
+ ]
654
+ : [`Move mode — ${selected ?? "segment"}`, "↑↓ move · Enter/Space finish", cancellation];
655
+ }
656
+ return width < 30
657
+ ? ["↑↓ navigate", "Enter/Space toggle", "M move · B line break", "Alt+↑/↓ quick move", `${closeKeys} close`]
658
+ : [
659
+ "↑↓ navigate · Enter/Space show/hide · M move mode",
660
+ `B add/remove line break after · Alt+↑/↓ quick move · ${closeKeys} close`,
661
+ ];
704
662
  }
705
663
 
706
664
  function displayKey(key: string): string {
707
- return sanitizeTerminalText(key)
708
- .split("+")
709
- .map((part) => {
710
- if (part === "escape") return "Esc";
711
- if (part === "ctrl") return "Ctrl";
712
- if (part === "alt") return process.platform === "darwin" ? "Option" : "Alt";
713
- if (part === "shift") return "Shift";
714
- return part;
715
- })
716
- .join("+");
665
+ return sanitizeTerminalText(key)
666
+ .split("+")
667
+ .map((part) => {
668
+ if (part === "escape") return "Esc";
669
+ if (part === "ctrl") return "Ctrl";
670
+ if (part === "alt") return process.platform === "darwin" ? "Option" : "Alt";
671
+ if (part === "shift") return "Shift";
672
+ return part;
673
+ })
674
+ .join("+");
717
675
  }
718
676
 
719
677
  function segmentsDocument(
720
- current: LoadedStatuslineSettings,
721
- name: SegmentName,
722
- shouldShow: boolean,
678
+ current: LoadedStatuslineSettings,
679
+ name: SegmentName,
680
+ shouldShow: boolean,
723
681
  ): { nextDocument: string; previousDocument: string } {
724
- const { parsed, rawDocument: previousDocument } = editableSettings(current, "changing segments");
725
- const segments = shouldShow
726
- ? [...current.config.segments, ...(current.config.segments.includes(name) ? [] : [name])]
727
- : current.config.segments.filter((segment) => segment !== name);
728
- parsed.segments = normalizeLineBreaks(segments);
729
- return {
730
- nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
731
- previousDocument,
732
- };
682
+ const { parsed, rawDocument: previousDocument } = editableSettings(current, "changing segments");
683
+ const segments = shouldShow
684
+ ? [...current.config.segments, ...(current.config.segments.includes(name) ? [] : [name])]
685
+ : current.config.segments.filter((segment) => segment !== name);
686
+ parsed.segments = normalizeLineBreaks(segments);
687
+ return {
688
+ nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
689
+ previousDocument,
690
+ };
733
691
  }
734
692
 
735
693
  function lineBreakAfterDocument(
736
- current: LoadedStatuslineSettings,
737
- name: SegmentName,
694
+ current: LoadedStatuslineSettings,
695
+ name: SegmentName,
738
696
  ): { nextDocument: string; previousDocument: string } {
739
- const { parsed, rawDocument: previousDocument } = editableSettings(
740
- current,
741
- "changing line breaks",
742
- );
743
- const segments = [...current.config.segments];
744
- const segmentIndex = segments.indexOf(name);
745
- if (segments[segmentIndex + 1] === LINE_BREAK_SEGMENT_NAME) {
746
- segments.splice(segmentIndex + 1, 1);
747
- } else {
748
- segments.splice(segmentIndex + 1, 0, LINE_BREAK_SEGMENT_NAME);
749
- }
750
- parsed.segments = segments;
751
- return {
752
- nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
753
- previousDocument,
754
- };
697
+ const { parsed, rawDocument: previousDocument } = editableSettings(current, "changing line breaks");
698
+ const segments = [...current.config.segments];
699
+ const segmentIndex = segments.indexOf(name);
700
+ if (segments[segmentIndex + 1] === LINE_BREAK_SEGMENT_NAME) {
701
+ segments.splice(segmentIndex + 1, 1);
702
+ } else {
703
+ segments.splice(segmentIndex + 1, 0, LINE_BREAK_SEGMENT_NAME);
704
+ }
705
+ parsed.segments = segments;
706
+ return {
707
+ nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
708
+ previousDocument,
709
+ };
755
710
  }
756
711
 
757
712
  function reorderedSegmentsDocument(
758
- current: LoadedStatuslineSettings,
759
- name: SegmentName,
760
- direction: -1 | 1,
713
+ current: LoadedStatuslineSettings,
714
+ name: SegmentName,
715
+ direction: -1 | 1,
761
716
  ): { nextDocument: string; previousDocument: string } | undefined {
762
- const dataIndexes = current.config.segments.flatMap((segment, index) =>
763
- segment === LINE_BREAK_SEGMENT_NAME ? [] : [index],
764
- );
765
- const currentDataIndex = dataIndexes.findIndex(
766
- (index) => current.config.segments[index] === name,
767
- );
768
- const targetDataIndex = currentDataIndex + direction;
769
- if (currentDataIndex < 0 || targetDataIndex < 0 || targetDataIndex >= dataIndexes.length) {
770
- return undefined;
771
- }
772
- const { parsed, rawDocument: previousDocument } = editableSettings(
773
- current,
774
- "reordering segments",
775
- );
776
- const segments = [...current.config.segments];
777
- const currentIndex = dataIndexes[currentDataIndex];
778
- const targetIndex = dataIndexes[targetDataIndex];
779
- if (currentIndex === undefined || targetIndex === undefined) return undefined;
780
- [segments[currentIndex], segments[targetIndex]] = [segments[targetIndex], segments[currentIndex]];
781
- parsed.segments = segments;
782
- return {
783
- nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
784
- previousDocument,
785
- };
717
+ const dataIndexes = current.config.segments.flatMap((segment, index) =>
718
+ segment === LINE_BREAK_SEGMENT_NAME ? [] : [index],
719
+ );
720
+ const currentDataIndex = dataIndexes.findIndex((index) => current.config.segments[index] === name);
721
+ const targetDataIndex = currentDataIndex + direction;
722
+ if (currentDataIndex < 0 || targetDataIndex < 0 || targetDataIndex >= dataIndexes.length) {
723
+ return undefined;
724
+ }
725
+ const { parsed, rawDocument: previousDocument } = editableSettings(current, "reordering segments");
726
+ const segments = [...current.config.segments];
727
+ const currentIndex = dataIndexes[currentDataIndex];
728
+ const targetIndex = dataIndexes[targetDataIndex];
729
+ if (currentIndex === undefined || targetIndex === undefined) return undefined;
730
+ [segments[currentIndex], segments[targetIndex]] = [segments[targetIndex], segments[currentIndex]];
731
+ parsed.segments = segments;
732
+ return {
733
+ nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
734
+ previousDocument,
735
+ };
786
736
  }
787
737
 
788
738
  function applySegmentsDocumentChange(
789
- change: { nextDocument: string; previousDocument: string },
790
- ctx: ExtensionCommandContext,
791
- options: StatuslineCommandOptions,
739
+ change: { nextDocument: string; previousDocument: string },
740
+ ctx: ExtensionCommandContext,
741
+ options: StatuslineCommandOptions,
792
742
  ): LoadedStatuslineSettings {
793
- const previous = options.getLoaded();
794
- const save = options.save ?? saveStatuslineSettingsDocument;
795
- const next = save(options.settingsPath, change.nextDocument);
796
- try {
797
- options.apply(next, ctx);
798
- } catch (applyError) {
799
- const rollbackError = restoreStatuslineSettings(ctx, options, previous, next);
800
- if (rollbackError) {
801
- throw new Error(
802
- `runtime update failed: ${formatError(applyError)}; rollback failed: ${formatError(rollbackError)}`,
803
- );
804
- }
805
- throw applyError;
806
- }
807
- return next;
743
+ const previous = options.getLoaded();
744
+ const save = options.save ?? saveStatuslineSettingsDocument;
745
+ const next = save(options.settingsPath, change.nextDocument);
746
+ try {
747
+ options.apply(next, ctx);
748
+ } catch (applyError) {
749
+ const rollbackError = restoreStatuslineSettings(ctx, options, previous, next);
750
+ if (rollbackError) {
751
+ throw new Error(
752
+ `runtime update failed: ${formatError(applyError)}; rollback failed: ${formatError(rollbackError)}`,
753
+ );
754
+ }
755
+ throw applyError;
756
+ }
757
+ return next;
808
758
  }
809
759
 
810
760
  function restoreStatuslineSettings(
811
- ctx: ExtensionCommandContext,
812
- options: StatuslineCommandOptions,
813
- previous: LoadedStatuslineSettings,
814
- saved: LoadedStatuslineSettings,
761
+ ctx: ExtensionCommandContext,
762
+ options: StatuslineCommandOptions,
763
+ previous: LoadedStatuslineSettings,
764
+ saved: LoadedStatuslineSettings,
815
765
  ): unknown {
816
- try {
817
- if (saved.rawDocument === undefined || saved.fileIdentity === undefined) {
818
- throw new Error("The saved settings document identity is unavailable");
819
- }
820
- removeStatuslineSettingsDocumentIfMatches(
821
- options.settingsPath,
822
- saved.rawDocument,
823
- saved.fileIdentity,
824
- );
825
- if (previous.rawDocument === undefined || previous.settingsPath !== options.settingsPath) {
826
- options.apply(previous, ctx);
827
- return undefined;
828
- }
829
- const restored = (options.save ?? saveStatuslineSettingsDocument)(
830
- options.settingsPath,
831
- previous.rawDocument,
832
- );
833
- options.apply(restored, ctx);
834
- return undefined;
835
- } catch (error) {
836
- return error;
837
- }
766
+ try {
767
+ if (saved.rawDocument === undefined || saved.fileIdentity === undefined) {
768
+ throw new Error("The saved settings document identity is unavailable");
769
+ }
770
+ removeStatuslineSettingsDocumentIfMatches(options.settingsPath, saved.rawDocument, saved.fileIdentity);
771
+ if (previous.rawDocument === undefined || previous.settingsPath !== options.settingsPath) {
772
+ options.apply(previous, ctx);
773
+ return undefined;
774
+ }
775
+ const restored = (options.save ?? saveStatuslineSettingsDocument)(options.settingsPath, previous.rawDocument);
776
+ options.apply(restored, ctx);
777
+ return undefined;
778
+ } catch (error) {
779
+ return error;
780
+ }
838
781
  }
839
782
 
840
783
  function normalizeLineBreaks(segments: readonly ConfigSegmentName[]): ConfigSegmentName[] {
841
- const normalized: ConfigSegmentName[] = [];
842
- for (const segment of segments) {
843
- if (
844
- segment === LINE_BREAK_SEGMENT_NAME &&
845
- (normalized.length === 0 || normalized.at(-1) === LINE_BREAK_SEGMENT_NAME)
846
- ) {
847
- continue;
848
- }
849
- normalized.push(segment);
850
- }
851
- if (normalized.at(-1) === LINE_BREAK_SEGMENT_NAME) normalized.pop();
852
- return normalized;
784
+ const normalized: ConfigSegmentName[] = [];
785
+ for (const segment of segments) {
786
+ if (
787
+ segment === LINE_BREAK_SEGMENT_NAME &&
788
+ (normalized.length === 0 || normalized.at(-1) === LINE_BREAK_SEGMENT_NAME)
789
+ ) {
790
+ continue;
791
+ }
792
+ normalized.push(segment);
793
+ }
794
+ if (normalized.at(-1) === LINE_BREAK_SEGMENT_NAME) normalized.pop();
795
+ return normalized;
853
796
  }
854
797
 
855
798
  function editableSettings(
856
- current: LoadedStatuslineSettings,
857
- action: string,
799
+ current: LoadedStatuslineSettings,
800
+ action: string,
858
801
  ): { parsed: Record<string, unknown>; rawDocument: string } {
859
- const rawDocument =
860
- current.source === "user" && current.rawDocument !== undefined
861
- ? current.rawDocument
862
- : current.source === "built-in" &&
863
- current.rawDocument === undefined &&
864
- current.diagnostics.length === 0
865
- ? DEFAULT_STATUSLINE_DOCUMENT
866
- : undefined;
867
- if (rawDocument === undefined || current.diagnostics.some((item) => item.code !== "unknown")) {
868
- throw new Error(`Fix pi-statusline.json before ${action}`);
869
- }
870
- const parsed = JSON.parse(rawDocument) as unknown;
871
- if (!isRecord(parsed)) throw new Error("Settings must contain a JSON object");
872
- return { parsed, rawDocument };
802
+ const rawDocument =
803
+ current.source === "user" && current.rawDocument !== undefined
804
+ ? current.rawDocument
805
+ : current.source === "built-in" && current.rawDocument === undefined && current.diagnostics.length === 0
806
+ ? DEFAULT_STATUSLINE_DOCUMENT
807
+ : undefined;
808
+ if (rawDocument === undefined || current.diagnostics.some((item) => item.code !== "unknown")) {
809
+ throw new Error(`Fix pi-statusline.json before ${action}`);
810
+ }
811
+ const parsed = JSON.parse(rawDocument) as unknown;
812
+ if (!isRecord(parsed)) throw new Error("Settings must contain a JSON object");
813
+ return { parsed, rawDocument };
873
814
  }
874
815
 
875
816
  function showStatus(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
876
- if (!canNotify(ctx)) return;
877
- const loaded = options.getLoaded();
878
- const diagnostics = loaded.diagnostics
879
- .slice(0, 5)
880
- .map((item) => `${item.path || "root"}: ${item.message}`)
881
- .join("; ");
882
- ctx.ui.notify(
883
- [
884
- `pi-statusline source: ${loaded.source}`,
885
- `active path: ${loaded.settingsPath}`,
886
- `save target: ${options.settingsPath}`,
887
- `palette preset: ${loaded.config.palettePreset}`,
888
- `density: ${loaded.config.density}`,
889
- `separator: ${loaded.config.separator}`,
890
- `information: ${inferInformationProfile(loaded.config.segments)}`,
891
- `segments: ${loaded.config.segments.join(", ") || "none"}`,
892
- diagnostics ? `warnings: ${diagnostics}` : "warnings: none",
893
- ].join("\n"),
894
- loaded.diagnostics.length > 0 ? "warning" : "info",
895
- );
817
+ if (!canNotify(ctx)) return;
818
+ const loaded = options.getLoaded();
819
+ const diagnostics = loaded.diagnostics
820
+ .slice(0, 5)
821
+ .map((item) => `${item.path || "root"}: ${item.message}`)
822
+ .join("; ");
823
+ ctx.ui.notify(
824
+ [
825
+ `pi-statusline source: ${loaded.source}`,
826
+ `active path: ${loaded.settingsPath}`,
827
+ `save target: ${options.settingsPath}`,
828
+ `palette preset: ${loaded.config.palettePreset}`,
829
+ `density: ${loaded.config.density}`,
830
+ `separator: ${loaded.config.separator}`,
831
+ `information: ${inferInformationProfile(loaded.config.segments)}`,
832
+ `segments: ${loaded.config.segments.join(", ") || "none"}`,
833
+ diagnostics ? `warnings: ${diagnostics}` : "warnings: none",
834
+ ].join("\n"),
835
+ loaded.diagnostics.length > 0 ? "warning" : "info",
836
+ );
896
837
  }
897
838
 
898
839
  function showHelp(ctx: ExtensionCommandContext, settingsPath: string) {
899
- if (!canNotify(ctx)) return;
900
- ctx.ui.notify(
901
- [
902
- "/statusline — open Appearance, Information, Advanced, Status, and Help",
903
- "/statusline settings — edit and apply JSON",
904
- "/statusline status — show source, path, information level, and warnings",
905
- "/statusline help — show this help",
906
- "Menu actions: Appearance, Information, Advanced, Status, Help.",
907
- "Information levels: minimal, balanced, detailed; any other segment array is custom.",
908
- "Advanced actions: Custom layout, Edit settings JSON, Back.",
909
- `Settings: ${settingsPath}`,
910
- "Fields: palettePreset, palette, density, separator, segments, segmentText, extensionStatusIcons",
911
- "Named presets ignore but preserve palette; custom uses its per-segment fg/bg colors.",
912
- "Responsive rows retain context, model, location, and active work before decorative data.",
913
- "Custom layout can show, hide, reorder, or split data segments across rows.",
914
- "Press M for move mode, Alt+Up/Alt+Down for quick move, and B for a line break.",
915
- "Line breaks (line_break) may repeat when separated by data segments, but cannot be consecutive.",
916
- "segmentText supports prefix and suffix strings around Pi-owned dynamic values.",
917
- "segmentText.model also supports truncationLength, truncationSymbol, and truncationDirection (start, middle, end).",
918
- ].join("\n"),
919
- "info",
920
- );
840
+ if (!canNotify(ctx)) return;
841
+ ctx.ui.notify(
842
+ [
843
+ "/statusline — open Appearance, Information, Advanced, Status, and Help",
844
+ "/statusline settings — edit and apply JSON",
845
+ "/statusline status — show source, path, information level, and warnings",
846
+ "/statusline help — show this help",
847
+ "Menu actions: Appearance, Information, Advanced, Status, Help.",
848
+ "Information levels: minimal, balanced, detailed; any other segment array is custom.",
849
+ "Advanced actions: Custom layout, Edit settings JSON, Back.",
850
+ `Settings: ${settingsPath}`,
851
+ "Fields: palettePreset, palette, density, separator, segments, segmentText, extensionStatusIcons",
852
+ "Named presets ignore but preserve palette; custom uses its per-segment fg/bg colors.",
853
+ "Responsive rows retain context, model, location, and active work before decorative data.",
854
+ "Custom layout can show, hide, reorder, or split data segments across rows.",
855
+ "Press M for move mode, Alt+Up/Alt+Down for quick move, and B for a line break.",
856
+ "Line breaks (line_break) may repeat when separated by data segments, but cannot be consecutive.",
857
+ "segmentText supports prefix and suffix strings around Pi-owned dynamic values.",
858
+ "segmentText.model also supports truncationLength, truncationSymbol, and truncationDirection (start, middle, end).",
859
+ ].join("\n"),
860
+ "info",
861
+ );
921
862
  }
922
863
 
923
864
  function isRecord(value: unknown): value is Record<string, unknown> {
924
- return typeof value === "object" && value !== null && !Array.isArray(value);
865
+ return typeof value === "object" && value !== null && !Array.isArray(value);
925
866
  }
926
867
 
927
868
  function isPaletteName(value: PalettePreset): value is PaletteName {
928
- return (PALETTE_NAMES as readonly PalettePreset[]).includes(value);
869
+ return (PALETTE_NAMES as readonly PalettePreset[]).includes(value);
929
870
  }
930
871
 
931
872
  function canNotify(ctx: ExtensionCommandContext): boolean {
932
- return ctx.mode === "tui" || ctx.hasUI;
873
+ return ctx.mode === "tui" || ctx.hasUI;
933
874
  }
934
875
 
935
876
  function formatError(error: unknown): string {
936
- return error instanceof Error ? error.message : String(error);
877
+ return error instanceof Error ? error.message : String(error);
937
878
  }