pi-formatter 0.2.0 → 1.0.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 +35 -8
- package/extensions/formatter/config.ts +55 -11
- package/extensions/formatter/context.ts +8 -4
- package/extensions/formatter/path.ts +5 -2
- package/extensions/index.ts +281 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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 once per turn. You can also format after each tool
|
|
7
|
+
call or defer formatting until the current session shuts down.
|
|
8
|
+
|
|
9
|
+
This default changed from the previous immediate-per-tool behavior. If you want
|
|
10
|
+
the old default, set `"formatMode": "tool"`.
|
|
9
11
|
|
|
10
12
|
## 📦 Install
|
|
11
13
|
|
|
@@ -15,8 +17,24 @@ pi install npm:pi-formatter
|
|
|
15
17
|
|
|
16
18
|
## ⚙️ What it does
|
|
17
19
|
|
|
18
|
-
`pi-formatter`
|
|
19
|
-
best-effort
|
|
20
|
+
`pi-formatter` detects file types and runs the appropriate formatter as
|
|
21
|
+
best-effort post-processing. Formatter failures never block tool results.
|
|
22
|
+
|
|
23
|
+
Formatting modes:
|
|
24
|
+
|
|
25
|
+
- `tool`: format immediately after each successful `write` or `edit` tool
|
|
26
|
+
result.
|
|
27
|
+
Use this mode when you want the file on disk to stay formatted after every
|
|
28
|
+
edit, even while the agent is still working.
|
|
29
|
+
- `turn`: collect files touched during the current turn and format them once at
|
|
30
|
+
`turn_end`. This is the default.
|
|
31
|
+
Use this mode when you want to avoid mid-turn formatter drift while still
|
|
32
|
+
keeping files formatted throughout the run.
|
|
33
|
+
- `session`: collect files touched during the current session and format them
|
|
34
|
+
once at `session_shutdown`.
|
|
35
|
+
Use this mode when you want the fewest formatter interruptions and are okay
|
|
36
|
+
with formatting only when the session exits, reloads, or switches. Interrupted
|
|
37
|
+
runs stay pending until the session ends or changes.
|
|
20
38
|
|
|
21
39
|
Supported file types:
|
|
22
40
|
|
|
@@ -31,6 +49,11 @@ Supported file types:
|
|
|
31
49
|
For JS/TS and JSON, project-configured tools are preferred first (Biome,
|
|
32
50
|
ESLint), with Prettier as a fallback.
|
|
33
51
|
|
|
52
|
+
## 🎮 Commands
|
|
53
|
+
|
|
54
|
+
- `/formatter`: open the interactive formatter settings editor and save changes
|
|
55
|
+
to `formatter.json`
|
|
56
|
+
|
|
34
57
|
## 🔧 Configuration
|
|
35
58
|
|
|
36
59
|
Create `<agent-dir>/formatter.json`, where `<agent-dir>` is pi's agent config
|
|
@@ -38,13 +61,17 @@ folder (default: `~/.pi/agent`, overridable via `PI_CODING_AGENT_DIR`):
|
|
|
38
61
|
|
|
39
62
|
```json
|
|
40
63
|
{
|
|
64
|
+
"formatMode": "turn",
|
|
41
65
|
"commandTimeoutMs": 10000,
|
|
42
66
|
"hideCallSummariesInTui": false
|
|
43
67
|
}
|
|
44
68
|
```
|
|
45
69
|
|
|
70
|
+
- `formatMode`: formatting strategy (`"tool"` | `"turn"` | `"session"`,
|
|
71
|
+
default: `"turn"`). Use `"tool"` to restore the old immediate default.
|
|
46
72
|
- `commandTimeoutMs`: timeout (ms) per formatter command (default: `10000`)
|
|
47
|
-
- `hideCallSummariesInTui`: hide formatter pass/fail summaries in the TUI
|
|
73
|
+
- `hideCallSummariesInTui`: hide formatter pass/fail summaries in the TUI
|
|
74
|
+
(default: `false`)
|
|
48
75
|
|
|
49
76
|
## 🧩 Adding formatters
|
|
50
77
|
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
|
|
5
|
+
export type FormatMode = "tool" | "turn" | "session";
|
|
6
|
+
|
|
5
7
|
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
6
8
|
const DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI = false;
|
|
9
|
+
const DEFAULT_FORMAT_MODE: FormatMode = "turn";
|
|
7
10
|
const FORMATTER_CONFIG_FILE = "formatter.json";
|
|
8
11
|
|
|
9
|
-
type
|
|
12
|
+
export type FormatterConfigSnapshot = {
|
|
10
13
|
commandTimeoutMs: number;
|
|
11
14
|
hideCallSummariesInTui: boolean;
|
|
15
|
+
formatMode: FormatMode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_FORMATTER_CONFIG: FormatterConfigSnapshot = {
|
|
19
|
+
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
20
|
+
hideCallSummariesInTui: DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
21
|
+
formatMode: DEFAULT_FORMAT_MODE,
|
|
12
22
|
};
|
|
13
23
|
|
|
14
|
-
function getFormatterConfigPath(): string {
|
|
24
|
+
export function getFormatterConfigPath(): string {
|
|
15
25
|
return join(getAgentDir(), FORMATTER_CONFIG_FILE);
|
|
16
26
|
}
|
|
17
27
|
|
|
@@ -54,14 +64,34 @@ function parseBooleanValue(value: unknown, defaultValue: boolean): boolean {
|
|
|
54
64
|
return value;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
function
|
|
67
|
+
function parseFormatMode(value: unknown, defaultValue: FormatMode): FormatMode {
|
|
68
|
+
if (value === "tool" || value === "turn" || value === "session") {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return defaultValue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toFormatterConfigObject(
|
|
76
|
+
config: FormatterConfigSnapshot,
|
|
77
|
+
): Record<string, unknown> {
|
|
78
|
+
return {
|
|
79
|
+
commandTimeoutMs: config.commandTimeoutMs,
|
|
80
|
+
hideCallSummariesInTui: config.hideCallSummariesInTui,
|
|
81
|
+
formatMode: config.formatMode,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeFormatterConfigFile(content: string): void {
|
|
86
|
+
mkdirSync(getAgentDir(), { recursive: true });
|
|
87
|
+
writeFileSync(getFormatterConfigPath(), content, "utf8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function loadFormatterConfig(): FormatterConfigSnapshot {
|
|
58
91
|
const config = readJsonObject(getFormatterConfigPath());
|
|
59
92
|
|
|
60
93
|
if (!config) {
|
|
61
|
-
return {
|
|
62
|
-
commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
|
|
63
|
-
hideCallSummariesInTui: DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
64
|
-
};
|
|
94
|
+
return { ...DEFAULT_FORMATTER_CONFIG };
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
return {
|
|
@@ -73,10 +103,24 @@ function loadFormatterConfig(): FormatterConfig {
|
|
|
73
103
|
config.hideCallSummariesInTui,
|
|
74
104
|
DEFAULT_HIDE_CALL_SUMMARIES_IN_TUI,
|
|
75
105
|
),
|
|
106
|
+
formatMode: parseFormatMode(config.formatMode, DEFAULT_FORMAT_MODE),
|
|
76
107
|
};
|
|
77
108
|
}
|
|
78
109
|
|
|
79
|
-
|
|
110
|
+
export function cloneFormatterConfig(
|
|
111
|
+
config: FormatterConfigSnapshot,
|
|
112
|
+
): FormatterConfigSnapshot {
|
|
113
|
+
return {
|
|
114
|
+
commandTimeoutMs: config.commandTimeoutMs,
|
|
115
|
+
hideCallSummariesInTui: config.hideCallSummariesInTui,
|
|
116
|
+
formatMode: config.formatMode,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
80
119
|
|
|
81
|
-
export
|
|
82
|
-
|
|
120
|
+
export function writeFormatterConfigSnapshot(
|
|
121
|
+
config: FormatterConfigSnapshot,
|
|
122
|
+
): void {
|
|
123
|
+
writeFormatterConfigFile(
|
|
124
|
+
`${JSON.stringify(toFormatterConfigObject(config), null, 2)}\n`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getRelativePathOrAbsolute,
|
|
6
|
+
isWithinDirectory,
|
|
7
|
+
pathExists,
|
|
8
|
+
} from "./path.js";
|
|
5
9
|
import { hasCommand } from "./system.js";
|
|
6
10
|
import type { FileKind, RequiredMajorVersion, RunnerContext } from "./types.js";
|
|
7
11
|
|
|
@@ -239,12 +243,12 @@ export class FormatRunContext implements RunnerContext {
|
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
private async resolveChangedLines(): Promise<string[]> {
|
|
242
|
-
const
|
|
246
|
+
const diffPath = getRelativePathOrAbsolute(this.filePath, this.cwd);
|
|
243
247
|
const rangeSet = new Set<string>();
|
|
244
248
|
|
|
245
249
|
const diffArgSets = [
|
|
246
|
-
["diff", "--unified=0", "--",
|
|
247
|
-
["diff", "--cached", "--unified=0", "--",
|
|
250
|
+
["diff", "--unified=0", "--", diffPath],
|
|
251
|
+
["diff", "--cached", "--unified=0", "--", diffPath],
|
|
248
252
|
];
|
|
249
253
|
|
|
250
254
|
for (const args of diffArgSets) {
|
|
@@ -54,8 +54,11 @@ export function isWithinDirectory(
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function
|
|
58
|
-
|
|
57
|
+
export function getRelativePathOrAbsolute(
|
|
58
|
+
filePath: string,
|
|
59
|
+
directory: string,
|
|
60
|
+
): string {
|
|
61
|
+
const relPath = relative(directory, filePath);
|
|
59
62
|
if (
|
|
60
63
|
!relPath ||
|
|
61
64
|
relPath === "." ||
|
package/extensions/index.ts
CHANGED
|
@@ -1,49 +1,119 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import {
|
|
3
2
|
type ExtensionAPI,
|
|
3
|
+
getSettingsListTheme,
|
|
4
4
|
isEditToolResult,
|
|
5
5
|
isWriteToolResult,
|
|
6
6
|
} from "@mariozechner/pi-coding-agent";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Container,
|
|
9
|
+
type SettingItem,
|
|
10
|
+
SettingsList,
|
|
11
|
+
Text,
|
|
12
|
+
} from "@mariozechner/pi-tui";
|
|
13
|
+
import {
|
|
14
|
+
cloneFormatterConfig,
|
|
15
|
+
type FormatterConfigSnapshot,
|
|
16
|
+
getFormatterConfigPath,
|
|
17
|
+
loadFormatterConfig,
|
|
18
|
+
writeFormatterConfigSnapshot,
|
|
10
19
|
} from "./formatter/config.js";
|
|
11
20
|
import { type FormatCallSummary, formatFile } from "./formatter/dispatch.js";
|
|
12
21
|
import {
|
|
13
|
-
|
|
22
|
+
getRelativePathOrAbsolute,
|
|
14
23
|
pathExists,
|
|
15
24
|
resolveToolPath,
|
|
16
25
|
} from "./formatter/path.js";
|
|
17
26
|
|
|
27
|
+
function normalizeSummaryMessage(message: string): string {
|
|
28
|
+
return message.replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
function formatError(error: unknown): string {
|
|
19
32
|
return error instanceof Error ? error.message : String(error);
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
35
|
+
function formatCallSummary(
|
|
36
|
+
summary: FormatCallSummary,
|
|
37
|
+
fileLabel: string,
|
|
38
|
+
): string {
|
|
39
|
+
const prefix = summary.status === "succeeded" ? "✔︎" : "✘";
|
|
40
|
+
const base = `${prefix} ${summary.runnerId}: ${fileLabel}`;
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
42
|
+
if (summary.status === "succeeded") {
|
|
43
|
+
return base;
|
|
44
|
+
}
|
|
32
45
|
|
|
33
|
-
function formatCallFailureSummary(summary: FormatCallSummary): string {
|
|
34
46
|
if (summary.failureMessage) {
|
|
35
|
-
return
|
|
47
|
+
return `${base}: ${normalizeSummaryMessage(summary.failureMessage)}`;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
if (summary.exitCode !== undefined) {
|
|
39
|
-
return
|
|
51
|
+
return `${base} (exit ${summary.exitCode})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveEventPath(rawPath: unknown, cwd: string): string | undefined {
|
|
58
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return resolveToolPath(rawPath, cwd);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFormatterSettingItems(
|
|
66
|
+
config: FormatterConfigSnapshot,
|
|
67
|
+
): SettingItem[] {
|
|
68
|
+
const timeoutValues = ["2000", "5000", "10000", "30000", "60000"];
|
|
69
|
+
|
|
70
|
+
if (!timeoutValues.includes(String(config.commandTimeoutMs))) {
|
|
71
|
+
timeoutValues.push(String(config.commandTimeoutMs));
|
|
72
|
+
timeoutValues.sort(
|
|
73
|
+
(a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10),
|
|
74
|
+
);
|
|
40
75
|
}
|
|
41
76
|
|
|
42
|
-
return
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
id: "formatMode",
|
|
80
|
+
label: "Format mode",
|
|
81
|
+
description:
|
|
82
|
+
"Choose whether formatting runs after each successful write/edit tool call, once after each turn, or once when the session shuts down.",
|
|
83
|
+
currentValue: config.formatMode,
|
|
84
|
+
values: ["tool", "turn", "session"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "commandTimeoutMs",
|
|
88
|
+
label: "Command timeout",
|
|
89
|
+
description: "Maximum runtime per formatter command in milliseconds.",
|
|
90
|
+
currentValue: String(config.commandTimeoutMs),
|
|
91
|
+
values: timeoutValues,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "hideCallSummariesInTui",
|
|
95
|
+
label: "Hide TUI summaries",
|
|
96
|
+
description:
|
|
97
|
+
"Hide per-run formatter pass/fail summaries in the interactive UI.",
|
|
98
|
+
currentValue: config.hideCallSummariesInTui ? "on" : "off",
|
|
99
|
+
values: ["off", "on"],
|
|
100
|
+
},
|
|
101
|
+
];
|
|
43
102
|
}
|
|
44
103
|
|
|
104
|
+
type FormatterContext = {
|
|
105
|
+
cwd: string;
|
|
106
|
+
hasUI: boolean;
|
|
107
|
+
ui: {
|
|
108
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
45
112
|
export default function (pi: ExtensionAPI) {
|
|
113
|
+
let formatterConfig = loadFormatterConfig();
|
|
46
114
|
const formatQueueByPath = new Map<string, Promise<void>>();
|
|
115
|
+
const pendingTurnPaths = new Set<string>();
|
|
116
|
+
const pendingSessionPaths = new Set<string>();
|
|
47
117
|
|
|
48
118
|
const enqueueFormat = async (
|
|
49
119
|
filePath: string,
|
|
@@ -65,29 +135,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
135
|
await next;
|
|
66
136
|
};
|
|
67
137
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!isWriteToolResult(event) && !isEditToolResult(event)) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const rawPath = event.input.path;
|
|
78
|
-
if (rawPath.length === 0) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const filePath = resolveToolPath(rawPath, ctx.cwd);
|
|
83
|
-
|
|
138
|
+
const formatResolvedPath = async (
|
|
139
|
+
filePath: string,
|
|
140
|
+
ctx: FormatterContext,
|
|
141
|
+
): Promise<void> => {
|
|
84
142
|
if (!(await pathExists(filePath))) {
|
|
85
143
|
return;
|
|
86
144
|
}
|
|
87
145
|
|
|
88
|
-
const showSummaries = !hideCallSummariesInTui && ctx.hasUI;
|
|
146
|
+
const showSummaries = !formatterConfig.hideCallSummariesInTui && ctx.hasUI;
|
|
89
147
|
const notifyWarning = (message: string) => {
|
|
90
|
-
const normalizedMessage = message
|
|
148
|
+
const normalizedMessage = normalizeSummaryMessage(message);
|
|
91
149
|
|
|
92
150
|
if (ctx.hasUI) {
|
|
93
151
|
ctx.ui.notify(normalizedMessage, "warning");
|
|
@@ -105,24 +163,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
105
163
|
}
|
|
106
164
|
: undefined;
|
|
107
165
|
|
|
108
|
-
const runnerWarningReporter =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
: notifyWarning;
|
|
166
|
+
const runnerWarningReporter = showSummaries
|
|
167
|
+
? () => {
|
|
168
|
+
// Summary mode already reports failures compactly.
|
|
169
|
+
}
|
|
170
|
+
: notifyWarning;
|
|
114
171
|
|
|
115
172
|
try {
|
|
116
173
|
await formatFile(
|
|
117
174
|
pi,
|
|
118
175
|
ctx.cwd,
|
|
119
176
|
filePath,
|
|
120
|
-
commandTimeoutMs,
|
|
177
|
+
formatterConfig.commandTimeoutMs,
|
|
121
178
|
summaryReporter,
|
|
122
179
|
runnerWarningReporter,
|
|
123
180
|
);
|
|
124
181
|
} catch (error) {
|
|
125
|
-
const fileLabel =
|
|
182
|
+
const fileLabel = getRelativePathOrAbsolute(filePath, ctx.cwd);
|
|
126
183
|
notifyWarning(`Failed to format ${fileLabel}: ${formatError(error)}`);
|
|
127
184
|
}
|
|
128
185
|
|
|
@@ -130,14 +187,189 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
187
|
return;
|
|
131
188
|
}
|
|
132
189
|
|
|
133
|
-
|
|
134
|
-
if (summary.status === "succeeded") {
|
|
135
|
-
ctx.ui.notify(formatCallSuccessSummary(summary), "info");
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
190
|
+
const fileLabel = getRelativePathOrAbsolute(filePath, ctx.cwd);
|
|
138
191
|
|
|
139
|
-
|
|
192
|
+
for (const summary of summaries) {
|
|
193
|
+
ctx.ui.notify(formatCallSummary(summary, fileLabel), "info");
|
|
140
194
|
}
|
|
141
195
|
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const flushPaths = async (
|
|
199
|
+
paths: Set<string>,
|
|
200
|
+
ctx: FormatterContext,
|
|
201
|
+
): Promise<void> => {
|
|
202
|
+
const batch = [...paths];
|
|
203
|
+
paths.clear();
|
|
204
|
+
|
|
205
|
+
for (const filePath of batch) {
|
|
206
|
+
await formatResolvedPath(filePath, ctx);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const flushPendingPaths = async (ctx: FormatterContext): Promise<void> => {
|
|
211
|
+
await flushPaths(pendingTurnPaths, ctx);
|
|
212
|
+
await flushPaths(pendingSessionPaths, ctx);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const reloadFormatterConfig = () => {
|
|
216
|
+
formatterConfig = loadFormatterConfig();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
220
|
+
if (!isWriteToolResult(event) && !isEditToolResult(event)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (event.isError) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const filePath = resolveEventPath(event.input.path, ctx.cwd);
|
|
229
|
+
if (!filePath) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (formatterConfig.formatMode === "tool") {
|
|
234
|
+
await formatResolvedPath(filePath, ctx);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (formatterConfig.formatMode === "turn") {
|
|
239
|
+
pendingTurnPaths.add(filePath);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
pendingSessionPaths.add(filePath);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
247
|
+
if (pendingTurnPaths.size === 0) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await flushPaths(pendingTurnPaths, ctx);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
255
|
+
if (pendingTurnPaths.size === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await flushPaths(pendingTurnPaths, ctx);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
263
|
+
if (pendingTurnPaths.size === 0 && pendingSessionPaths.size === 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await flushPendingPaths(ctx);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
271
|
+
if (pendingTurnPaths.size === 0 && pendingSessionPaths.size === 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await flushPendingPaths(ctx);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
pi.registerCommand("formatter", {
|
|
279
|
+
description: "Configure formatter behavior.",
|
|
280
|
+
handler: async (_args, ctx) => {
|
|
281
|
+
if (!ctx.hasUI) {
|
|
282
|
+
console.warn("/formatter requires interactive UI mode");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const configPath = getFormatterConfigPath();
|
|
287
|
+
reloadFormatterConfig();
|
|
288
|
+
const draft = cloneFormatterConfig(formatterConfig);
|
|
289
|
+
|
|
290
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
291
|
+
const container = new Container();
|
|
292
|
+
container.addChild(
|
|
293
|
+
new Text(theme.fg("accent", theme.bold("Formatter Settings")), 1, 0),
|
|
294
|
+
);
|
|
295
|
+
container.addChild(new Text(theme.fg("dim", configPath), 1, 0));
|
|
296
|
+
container.addChild(new Text("", 0, 0));
|
|
297
|
+
|
|
298
|
+
const syncDraftToSettingsList = (settingsList: SettingsList) => {
|
|
299
|
+
settingsList.updateValue("formatMode", draft.formatMode);
|
|
300
|
+
settingsList.updateValue(
|
|
301
|
+
"commandTimeoutMs",
|
|
302
|
+
String(draft.commandTimeoutMs),
|
|
303
|
+
);
|
|
304
|
+
settingsList.updateValue(
|
|
305
|
+
"hideCallSummariesInTui",
|
|
306
|
+
draft.hideCallSummariesInTui ? "on" : "off",
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const settingsList = new SettingsList(
|
|
311
|
+
getFormatterSettingItems(draft),
|
|
312
|
+
8,
|
|
313
|
+
getSettingsListTheme(),
|
|
314
|
+
(id, newValue) => {
|
|
315
|
+
const previous = cloneFormatterConfig(draft);
|
|
316
|
+
|
|
317
|
+
if (id === "formatMode") {
|
|
318
|
+
draft.formatMode =
|
|
319
|
+
newValue as FormatterConfigSnapshot["formatMode"];
|
|
320
|
+
} else if (id === "commandTimeoutMs") {
|
|
321
|
+
draft.commandTimeoutMs = Number.parseInt(newValue, 10);
|
|
322
|
+
} else if (id === "hideCallSummariesInTui") {
|
|
323
|
+
draft.hideCallSummariesInTui = newValue === "on";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
writeFormatterConfigSnapshot(draft);
|
|
328
|
+
reloadFormatterConfig();
|
|
329
|
+
|
|
330
|
+
if (
|
|
331
|
+
id === "formatMode" &&
|
|
332
|
+
previous.formatMode !== draft.formatMode
|
|
333
|
+
) {
|
|
334
|
+
void flushPendingPaths(ctx).catch((error) => {
|
|
335
|
+
const message =
|
|
336
|
+
error instanceof Error ? error.message : String(error);
|
|
337
|
+
ctx.ui.notify(
|
|
338
|
+
`Failed to flush pending formats: ${message}`,
|
|
339
|
+
"error",
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const message =
|
|
345
|
+
error instanceof Error ? error.message : String(error);
|
|
346
|
+
draft.commandTimeoutMs = previous.commandTimeoutMs;
|
|
347
|
+
draft.hideCallSummariesInTui = previous.hideCallSummariesInTui;
|
|
348
|
+
draft.formatMode = previous.formatMode;
|
|
349
|
+
syncDraftToSettingsList(settingsList);
|
|
350
|
+
ctx.ui.notify(`Failed to save config: ${message}`, "error");
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
() => {
|
|
354
|
+
done(undefined);
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
container.addChild(settingsList);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
render(width: number) {
|
|
362
|
+
return container.render(width);
|
|
363
|
+
},
|
|
364
|
+
invalidate() {
|
|
365
|
+
container.invalidate();
|
|
366
|
+
},
|
|
367
|
+
handleInput(data: string) {
|
|
368
|
+
settingsList.handleInput?.(data);
|
|
369
|
+
tui.requestRender();
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
},
|
|
142
374
|
});
|
|
143
375
|
}
|