pi-formatter 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -8
- package/extensions/formatter/config.ts +69 -11
- package/extensions/index.ts +254 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# 🎨 pi-formatter
|
|
2
2
|
|
|
3
|
-
A [pi](https://pi.dev) extension that auto-formats files after
|
|
4
|
-
`edit` tool
|
|
3
|
+
A [pi](https://pi.dev) extension that auto-formats files after `write` and
|
|
4
|
+
`edit` tool calls.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
By default, formatting runs after each successful tool result. It can also be
|
|
7
|
+
configured to defer formatting until the agent stops and yields back to the
|
|
8
|
+
user.
|
|
9
9
|
|
|
10
10
|
## 📦 Install
|
|
11
11
|
|
|
@@ -15,8 +15,18 @@ pi install npm:pi-formatter
|
|
|
15
15
|
|
|
16
16
|
## ⚙️ What it does
|
|
17
17
|
|
|
18
|
-
`pi-formatter`
|
|
19
|
-
best-effort
|
|
18
|
+
`pi-formatter` detects file types and runs the appropriate formatter as
|
|
19
|
+
best-effort post-processing. Formatter failures never block tool results.
|
|
20
|
+
|
|
21
|
+
Formatting modes:
|
|
22
|
+
|
|
23
|
+
- `afterEachToolCall`: format immediately after each successful `write` or
|
|
24
|
+
`edit` tool result. This is the default.
|
|
25
|
+
- `afterAgentStop`: collect touched files during the run and format them once
|
|
26
|
+
the agent stops. This avoids mid-run model drift from formatter edits.
|
|
27
|
+
|
|
28
|
+
When `afterAgentStop` is active, interrupted or canceled runs are not formatted
|
|
29
|
+
unless `formatOnAbort` is enabled.
|
|
20
30
|
|
|
21
31
|
Supported file types:
|
|
22
32
|
|
|
@@ -31,6 +41,11 @@ Supported file types:
|
|
|
31
41
|
For JS/TS and JSON, project-configured tools are preferred first (Biome,
|
|
32
42
|
ESLint), with Prettier as a fallback.
|
|
33
43
|
|
|
44
|
+
## 🎮 Commands
|
|
45
|
+
|
|
46
|
+
- `/formatter`: open the interactive formatter settings editor and save changes
|
|
47
|
+
to `formatter.json`
|
|
48
|
+
|
|
34
49
|
## 🔧 Configuration
|
|
35
50
|
|
|
36
51
|
Create `<agent-dir>/formatter.json`, where `<agent-dir>` is pi's agent config
|
|
@@ -38,13 +53,20 @@ folder (default: `~/.pi/agent`, overridable via `PI_CODING_AGENT_DIR`):
|
|
|
38
53
|
|
|
39
54
|
```json
|
|
40
55
|
{
|
|
56
|
+
"formatMode": "afterEachToolCall",
|
|
57
|
+
"formatOnAbort": false,
|
|
41
58
|
"commandTimeoutMs": 10000,
|
|
42
59
|
"hideCallSummariesInTui": false
|
|
43
60
|
}
|
|
44
61
|
```
|
|
45
62
|
|
|
63
|
+
- `formatMode`: formatting strategy
|
|
64
|
+
(`"afterEachToolCall"` | `"afterAgentStop"`, default: `"afterEachToolCall"`)
|
|
65
|
+
- `formatOnAbort`: in deferred mode, also format files after an interrupted or
|
|
66
|
+
canceled run (default: `false`)
|
|
46
67
|
- `commandTimeoutMs`: timeout (ms) per formatter command (default: `10000`)
|
|
47
|
-
- `hideCallSummariesInTui`: hide formatter pass/fail summaries in the TUI
|
|
68
|
+
- `hideCallSummariesInTui`: hide formatter pass/fail summaries in the TUI
|
|
69
|
+
(default: `false`)
|
|
48
70
|
|
|
49
71
|
## 🧩 Adding formatters
|
|
50
72
|
|
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
2
7
|
import { join } from "node:path";
|
|
3
8
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
9
|
|
|
10
|
+
export type FormatMode = "afterEachToolCall" | "afterAgentStop";
|
|
11
|
+
|
|
5
12
|
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
6
13
|
const DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI = false;
|
|
14
|
+
const DEFAULT_FORMAT_MODE: FormatMode = "afterEachToolCall";
|
|
15
|
+
const DEFAULT_FORMAT_ON_ABORT = false;
|
|
7
16
|
const FORMATTER_CONFIG_FILE = "formatter.json";
|
|
8
17
|
|
|
9
|
-
type
|
|
18
|
+
export type FormatterConfigSnapshot = {
|
|
10
19
|
commandTimeoutMs: number;
|
|
11
20
|
hideCallSummariesInTui: boolean;
|
|
21
|
+
formatMode: FormatMode;
|
|
22
|
+
formatOnAbort: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_FORMATTER_CONFIG: FormatterConfigSnapshot = {
|
|
26
|
+
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
27
|
+
hideCallSummariesInTui: DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
28
|
+
formatMode: DEFAULT_FORMAT_MODE,
|
|
29
|
+
formatOnAbort: DEFAULT_FORMAT_ON_ABORT,
|
|
12
30
|
};
|
|
13
31
|
|
|
14
|
-
function getFormatterConfigPath(): string {
|
|
32
|
+
export function getFormatterConfigPath(): string {
|
|
15
33
|
return join(getAgentDir(), FORMATTER_CONFIG_FILE);
|
|
16
34
|
}
|
|
17
35
|
|
|
@@ -54,14 +72,35 @@ function parseBooleanValue(value: unknown, defaultValue: boolean): boolean {
|
|
|
54
72
|
return value;
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
function
|
|
75
|
+
function parseFormatMode(value: unknown, defaultValue: FormatMode): FormatMode {
|
|
76
|
+
if (value === "afterEachToolCall" || value === "afterAgentStop") {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return defaultValue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toFormatterConfigObject(
|
|
84
|
+
config: FormatterConfigSnapshot,
|
|
85
|
+
): Record<string, unknown> {
|
|
86
|
+
return {
|
|
87
|
+
commandTimeoutMs: config.commandTimeoutMs,
|
|
88
|
+
hideCallSummariesInTui: config.hideCallSummariesInTui,
|
|
89
|
+
formatMode: config.formatMode,
|
|
90
|
+
formatOnAbort: config.formatOnAbort,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeFormatterConfigFile(content: string): void {
|
|
95
|
+
mkdirSync(getAgentDir(), { recursive: true });
|
|
96
|
+
writeFileSync(getFormatterConfigPath(), content, "utf8");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function loadFormatterConfig(): FormatterConfigSnapshot {
|
|
58
100
|
const config = readJsonObject(getFormatterConfigPath());
|
|
59
101
|
|
|
60
102
|
if (!config) {
|
|
61
|
-
return {
|
|
62
|
-
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
63
|
-
hideCallSummariesInTui: DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
64
|
-
};
|
|
103
|
+
return { ...DEFAULT_FORMATTER_CONFIG };
|
|
65
104
|
}
|
|
66
105
|
|
|
67
106
|
return {
|
|
@@ -73,10 +112,29 @@ function loadFormatterConfig(): FormatterConfig {
|
|
|
73
112
|
config.hideCallSummariesInTui,
|
|
74
113
|
DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
75
114
|
),
|
|
115
|
+
formatMode: parseFormatMode(config.formatMode, DEFAULT_FORMAT_MODE),
|
|
116
|
+
formatOnAbort: parseBooleanValue(
|
|
117
|
+
config.formatOnAbort,
|
|
118
|
+
DEFAULT_FORMAT_ON_ABORT,
|
|
119
|
+
),
|
|
76
120
|
};
|
|
77
121
|
}
|
|
78
122
|
|
|
79
|
-
|
|
123
|
+
export function cloneFormatterConfig(
|
|
124
|
+
config: FormatterConfigSnapshot,
|
|
125
|
+
): FormatterConfigSnapshot {
|
|
126
|
+
return {
|
|
127
|
+
commandTimeoutMs: config.commandTimeoutMs,
|
|
128
|
+
hideCallSummariesInTui: config.hideCallSummariesInTui,
|
|
129
|
+
formatMode: config.formatMode,
|
|
130
|
+
formatOnAbort: config.formatOnAbort,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
80
133
|
|
|
81
|
-
export
|
|
82
|
-
|
|
134
|
+
export function writeFormatterConfigSnapshot(
|
|
135
|
+
config: FormatterConfigSnapshot,
|
|
136
|
+
): void {
|
|
137
|
+
writeFormatterConfigFile(
|
|
138
|
+
`${JSON.stringify(toFormatterConfigObject(config), null, 2)}\n`,
|
|
139
|
+
);
|
|
140
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import {
|
|
3
3
|
type ExtensionAPI,
|
|
4
|
+
getSettingsListTheme,
|
|
4
5
|
isEditToolResult,
|
|
5
6
|
isWriteToolResult,
|
|
6
7
|
} from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { Container, type SettingItem, SettingsList, Text } from "@mariozechner/pi-tui";
|
|
7
9
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
cloneFormatterConfig,
|
|
11
|
+
type FormatterConfigSnapshot,
|
|
12
|
+
getFormatterConfigPath,
|
|
13
|
+
loadFormatterConfig,
|
|
14
|
+
writeFormatterConfigSnapshot,
|
|
10
15
|
} from "./formatter/config.js";
|
|
11
16
|
import { type FormatCallSummary, formatFile } from "./formatter/dispatch.js";
|
|
12
17
|
import {
|
|
@@ -42,8 +47,66 @@ function formatCallFailureSummary(summary: FormatCallSummary): string {
|
|
|
42
47
|
return `✘ ${summary.runnerId}`;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
function resolveEventPath(rawPath: unknown, cwd: string): string | undefined {
|
|
51
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return resolveToolPath(rawPath, cwd);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getFormatterSettingItems(
|
|
59
|
+
config: FormatterConfigSnapshot,
|
|
60
|
+
): SettingItem[] {
|
|
61
|
+
const timeoutValues = ["2000", "5000", "10000", "30000", "60000"];
|
|
62
|
+
|
|
63
|
+
if (!timeoutValues.includes(String(config.commandTimeoutMs))) {
|
|
64
|
+
timeoutValues.push(String(config.commandTimeoutMs));
|
|
65
|
+
timeoutValues.sort(
|
|
66
|
+
(a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
id: "formatMode",
|
|
73
|
+
label: "Format mode",
|
|
74
|
+
description:
|
|
75
|
+
"Choose whether formatting runs after each successful write/edit tool call or once after the agent stops.",
|
|
76
|
+
currentValue: config.formatMode,
|
|
77
|
+
values: ["afterEachToolCall", "afterAgentStop"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "formatOnAbort",
|
|
81
|
+
label: "Format on abort",
|
|
82
|
+
description:
|
|
83
|
+
"When enabled, deferred formatting also runs if the model is interrupted or cancelled.",
|
|
84
|
+
currentValue: config.formatOnAbort ? "on" : "off",
|
|
85
|
+
values: ["off", "on"],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "commandTimeoutMs",
|
|
89
|
+
label: "Command timeout",
|
|
90
|
+
description: "Maximum runtime per formatter command in milliseconds.",
|
|
91
|
+
currentValue: String(config.commandTimeoutMs),
|
|
92
|
+
values: timeoutValues,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "hideCallSummariesInTui",
|
|
96
|
+
label: "Hide TUI summaries",
|
|
97
|
+
description:
|
|
98
|
+
"Hide per-run formatter pass/fail summaries in the interactive UI.",
|
|
99
|
+
currentValue: config.hideCallSummariesInTui ? "on" : "off",
|
|
100
|
+
values: ["off", "on"],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
45
105
|
export default function (pi: ExtensionAPI) {
|
|
106
|
+
let formatterConfig = loadFormatterConfig();
|
|
46
107
|
const formatQueueByPath = new Map<string, Promise<void>>();
|
|
108
|
+
const candidatePaths = new Set<string>();
|
|
109
|
+
const successfulPaths = new Set<string>();
|
|
47
110
|
|
|
48
111
|
const enqueueFormat = async (
|
|
49
112
|
filePath: string,
|
|
@@ -65,27 +128,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
128
|
await next;
|
|
66
129
|
};
|
|
67
130
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (rawPath.length === 0) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const filePath = resolveToolPath(rawPath, ctx.cwd);
|
|
83
|
-
|
|
131
|
+
const formatResolvedPath = async (
|
|
132
|
+
filePath: string,
|
|
133
|
+
ctx: {
|
|
134
|
+
cwd: string;
|
|
135
|
+
hasUI: boolean;
|
|
136
|
+
ui: {
|
|
137
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
): Promise<void> => {
|
|
84
141
|
if (!(await pathExists(filePath))) {
|
|
85
142
|
return;
|
|
86
143
|
}
|
|
87
144
|
|
|
88
|
-
const showSummaries = !hideCallSummariesInTui && ctx.hasUI;
|
|
145
|
+
const showSummaries = !formatterConfig.hideCallSummariesInTui && ctx.hasUI;
|
|
89
146
|
const notifyWarning = (message: string) => {
|
|
90
147
|
const normalizedMessage = message.replace(/\s+/g, " ").trim();
|
|
91
148
|
|
|
@@ -117,7 +174,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
117
174
|
pi,
|
|
118
175
|
ctx.cwd,
|
|
119
176
|
filePath,
|
|
120
|
-
commandTimeoutMs,
|
|
177
|
+
formatterConfig.commandTimeoutMs,
|
|
121
178
|
summaryReporter,
|
|
122
179
|
runnerWarningReporter,
|
|
123
180
|
);
|
|
@@ -139,5 +196,182 @@ export default function (pi: ExtensionAPI) {
|
|
|
139
196
|
ctx.ui.notify(formatCallFailureSummary(summary), "info");
|
|
140
197
|
}
|
|
141
198
|
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const reloadFormatterConfig = () => {
|
|
202
|
+
formatterConfig = loadFormatterConfig();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
pi.on("agent_start", async () => {
|
|
206
|
+
candidatePaths.clear();
|
|
207
|
+
successfulPaths.clear();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
211
|
+
if (formatterConfig.formatMode !== "afterAgentStop") {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (event.toolName !== "write" && event.toolName !== "edit") {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const filePath = resolveEventPath(event.input.path, ctx.cwd);
|
|
220
|
+
if (filePath) {
|
|
221
|
+
candidatePaths.add(filePath);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
226
|
+
if (!isWriteToolResult(event) && !isEditToolResult(event)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const filePath = resolveEventPath(event.input.path, ctx.cwd);
|
|
231
|
+
if (!filePath) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!event.isError) {
|
|
236
|
+
successfulPaths.add(filePath);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (formatterConfig.formatMode !== "afterEachToolCall" || event.isError) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await formatResolvedPath(filePath, ctx);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
247
|
+
if (formatterConfig.formatMode !== "afterAgentStop") {
|
|
248
|
+
candidatePaths.clear();
|
|
249
|
+
successfulPaths.clear();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let lastAssistantStopReason: string | undefined;
|
|
254
|
+
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
255
|
+
const message = event.messages[index];
|
|
256
|
+
if (message.role !== "assistant") {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
lastAssistantStopReason = message.stopReason;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const pathsToFormat = new Set<string>();
|
|
265
|
+
if (lastAssistantStopReason === "aborted") {
|
|
266
|
+
if (formatterConfig.formatOnAbort) {
|
|
267
|
+
for (const filePath of candidatePaths) {
|
|
268
|
+
pathsToFormat.add(filePath);
|
|
269
|
+
}
|
|
270
|
+
for (const filePath of successfulPaths) {
|
|
271
|
+
pathsToFormat.add(filePath);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
for (const filePath of successfulPaths) {
|
|
276
|
+
pathsToFormat.add(filePath);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
candidatePaths.clear();
|
|
281
|
+
successfulPaths.clear();
|
|
282
|
+
|
|
283
|
+
for (const filePath of pathsToFormat) {
|
|
284
|
+
await formatResolvedPath(filePath, ctx);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
pi.registerCommand("formatter", {
|
|
289
|
+
description: "Configure formatter behavior.",
|
|
290
|
+
handler: async (_args, ctx) => {
|
|
291
|
+
if (!ctx.hasUI) {
|
|
292
|
+
console.warn("/formatter requires interactive UI mode");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const configPath = getFormatterConfigPath();
|
|
297
|
+
reloadFormatterConfig();
|
|
298
|
+
const draft = cloneFormatterConfig(formatterConfig);
|
|
299
|
+
|
|
300
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
301
|
+
const container = new Container();
|
|
302
|
+
container.addChild(
|
|
303
|
+
new Text(theme.fg("accent", theme.bold("Formatter Settings")), 1, 0),
|
|
304
|
+
);
|
|
305
|
+
container.addChild(new Text(theme.fg("dim", configPath), 1, 0));
|
|
306
|
+
container.addChild(new Text("", 0, 0));
|
|
307
|
+
|
|
308
|
+
const syncDraftToSettingsList = (settingsList: SettingsList) => {
|
|
309
|
+
settingsList.updateValue("formatMode", draft.formatMode);
|
|
310
|
+
settingsList.updateValue(
|
|
311
|
+
"formatOnAbort",
|
|
312
|
+
draft.formatOnAbort ? "on" : "off",
|
|
313
|
+
);
|
|
314
|
+
settingsList.updateValue(
|
|
315
|
+
"commandTimeoutMs",
|
|
316
|
+
String(draft.commandTimeoutMs),
|
|
317
|
+
);
|
|
318
|
+
settingsList.updateValue(
|
|
319
|
+
"hideCallSummariesInTui",
|
|
320
|
+
draft.hideCallSummariesInTui ? "on" : "off",
|
|
321
|
+
);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const settingsList = new SettingsList(
|
|
325
|
+
getFormatterSettingItems(draft),
|
|
326
|
+
8,
|
|
327
|
+
getSettingsListTheme(),
|
|
328
|
+
(id, newValue) => {
|
|
329
|
+
const previous = cloneFormatterConfig(draft);
|
|
330
|
+
|
|
331
|
+
if (id === "formatMode") {
|
|
332
|
+
draft.formatMode = newValue as FormatterConfigSnapshot["formatMode"];
|
|
333
|
+
} else if (id === "formatOnAbort") {
|
|
334
|
+
draft.formatOnAbort = newValue === "on";
|
|
335
|
+
} else if (id === "commandTimeoutMs") {
|
|
336
|
+
draft.commandTimeoutMs = Number.parseInt(newValue, 10);
|
|
337
|
+
} else if (id === "hideCallSummariesInTui") {
|
|
338
|
+
draft.hideCallSummariesInTui = newValue === "on";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
writeFormatterConfigSnapshot(draft);
|
|
343
|
+
reloadFormatterConfig();
|
|
344
|
+
} catch (error) {
|
|
345
|
+
const message =
|
|
346
|
+
error instanceof Error ? error.message : String(error);
|
|
347
|
+
draft.commandTimeoutMs = previous.commandTimeoutMs;
|
|
348
|
+
draft.hideCallSummariesInTui = previous.hideCallSummariesInTui;
|
|
349
|
+
draft.formatMode = previous.formatMode;
|
|
350
|
+
draft.formatOnAbort = previous.formatOnAbort;
|
|
351
|
+
syncDraftToSettingsList(settingsList);
|
|
352
|
+
ctx.ui.notify(`Failed to save config: ${message}`, "error");
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
() => {
|
|
356
|
+
done(undefined);
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
container.addChild(settingsList);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
render(width: number) {
|
|
364
|
+
return container.render(width);
|
|
365
|
+
},
|
|
366
|
+
invalidate() {
|
|
367
|
+
container.invalidate();
|
|
368
|
+
},
|
|
369
|
+
handleInput(data: string) {
|
|
370
|
+
settingsList.handleInput?.(data);
|
|
371
|
+
tui.requestRender();
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
},
|
|
142
376
|
});
|
|
143
377
|
}
|