pi-tool-display 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-01
9
+
10
+ ### Added
11
+ - Public repository scaffolding (`README.md`, `LICENSE`, `CHANGELOG.md`, `.gitignore`, `.npmignore`).
12
+ - Package metadata for public distribution (`keywords`, `files`, `license`, `publishConfig`, engine constraints).
13
+ - Vendored `zellij-modal.ts` to keep this extension self-contained as a standalone repository.
14
+
15
+ ### Changed
16
+ - Updated `config-modal.ts` to use local `zellij-modal.ts` import.
17
+ - Updated build script to include `zellij-modal.ts`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MasuRii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # pi-tool-display
2
+
3
+ OpenCode-style tool rendering for the Pi coding agent.
4
+
5
+ `pi-tool-display` overrides built-in tool renderers so tool calls/results stay compact by default, with optional previews and richer diffs when you need detail.
6
+
7
+ ![Screenshot](pi-tool-display.png)
8
+
9
+ ## Features
10
+
11
+ - Compact rendering for `read`, `grep`, `find`, `ls`, `bash`, `edit`, and `write`
12
+ - Presets for output verbosity: `opencode`, `balanced`, `verbose`
13
+ - Interactive settings modal via `/tool-display`
14
+ - Command-based controls (`show`, `reset`, `preset ...`)
15
+ - Diff renderer with adaptive split/unified mode and inline highlights
16
+ - Optional truncation and RTK compaction hints
17
+ - Native bordered user message box styling
18
+
19
+ ## Installation
20
+
21
+ ### Local extension folder
22
+
23
+ Place this folder in:
24
+
25
+ - Global: `~/.pi/agent/extensions/pi-tool-display`
26
+ - Project: `.pi/extensions/pi-tool-display`
27
+
28
+ Pi auto-discovers this location. If you keep it elsewhere, add the path to your settings `extensions` array.
29
+
30
+ ### As a package (after publishing)
31
+
32
+ ```bash
33
+ pi install npm:pi-tool-display
34
+ ```
35
+
36
+ Or from git:
37
+
38
+ ```bash
39
+ pi install git:github.com/MasuRii/pi-tool-display
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Command:
45
+
46
+ ```text
47
+ /tool-display
48
+ ```
49
+
50
+ Direct args:
51
+
52
+ ```text
53
+ /tool-display show
54
+ /tool-display reset
55
+ /tool-display preset opencode
56
+ /tool-display preset balanced
57
+ /tool-display preset verbose
58
+ ```
59
+
60
+ ## Presets
61
+
62
+ - `opencode` (default): hides most tool output in collapsed view
63
+ - `balanced`: summary/count style output
64
+ - `verbose`: preview mode with larger collapsed output
65
+
66
+ ## Configuration
67
+
68
+ Runtime config is stored at:
69
+
70
+ ```text
71
+ ~/.pi/agent/extensions/pi-tool-display/config.json
72
+ ```
73
+
74
+ A starter file is included as `config.example.json`.
75
+
76
+ Values are normalized and clamped on load/save to avoid invalid settings.
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ npm run build
82
+ npm run lint
83
+ npm run test
84
+ npm run check
85
+ ```
86
+
87
+ ## Project Layout
88
+
89
+ - `index.ts` - extension bootstrap and registration
90
+ - `tool-overrides.ts` - built-in and MCP renderer overrides
91
+ - `diff-renderer.ts` - edit/write diff rendering engine
92
+ - `config-modal.ts` - `/tool-display` settings UI
93
+ - `config-store.ts` - config load/save and normalization
94
+ - `presets.ts` - preset definitions and matching
95
+ - `render-utils.ts` - shared rendering helpers
96
+ - `user-message-box-native.ts` - user message border patch
97
+ - `zellij-modal.ts` - vendored modal UI primitives used by settings UI
98
+
99
+ ## License
100
+
101
+ MIT
@@ -0,0 +1,345 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import type { SettingItem } from "@mariozechner/pi-tui";
3
+ import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.js";
4
+ import {
5
+ detectToolDisplayPreset,
6
+ getToolDisplayPresetConfig,
7
+ parseToolDisplayPreset,
8
+ TOOL_DISPLAY_PRESETS,
9
+ type ToolDisplayPreset,
10
+ } from "./presets.js";
11
+ import { type ToolDisplayConfig } from "./types.js";
12
+
13
+ interface ToolDisplayConfigController {
14
+ getConfig(): ToolDisplayConfig;
15
+ setConfig(next: ToolDisplayConfig, ctx: ExtensionCommandContext): void;
16
+ }
17
+
18
+ interface SettingValueSyncTarget {
19
+ updateValue(id: string, value: string): void;
20
+ }
21
+
22
+ const PREVIEW_LINE_VALUES = ["4", "8", "12", "20", "40"];
23
+ const EXPANDED_PREVIEW_MAX_LINE_VALUES = ["500", "1000", "2000", "4000", "8000", "12000", "20000"];
24
+ const BASH_PREVIEW_LINE_VALUES = ["0", "5", "10", "20", "40"];
25
+ const DIFF_SPLIT_MIN_WIDTH_VALUES = ["90", "100", "120", "140", "160"];
26
+ const DIFF_COLLAPSED_LINE_VALUES = ["8", "16", "24", "40", "80"];
27
+ const PRESET_COMMAND_HINT = TOOL_DISPLAY_PRESETS.join("|");
28
+
29
+ function toOnOff(value: boolean): string {
30
+ return value ? "on" : "off";
31
+ }
32
+
33
+ function summarizeConfig(config: ToolDisplayConfig): string {
34
+ const preset = detectToolDisplayPreset(config);
35
+ return `preset=${preset}, read=${config.readOutputMode}, search=${config.searchOutputMode}, mcp=${config.mcpOutputMode}, preview=${config.previewLines}, expandedMax=${config.expandedPreviewMaxLines}, bash=${config.bashCollapsedLines}, diff=${config.diffViewMode}@${config.diffSplitMinWidth}, diffLines=${config.diffCollapsedLines}, diffWrap=${toOnOff(config.diffWordWrap)}, rtkHints=${toOnOff(config.showRtkCompactionHints)}`;
36
+ }
37
+
38
+ function parseNumber(value: string, fallback: number): number {
39
+ const parsed = Number.parseInt(value, 10);
40
+ return Number.isNaN(parsed) ? fallback : parsed;
41
+ }
42
+
43
+ function buildSettingItems(config: ToolDisplayConfig): SettingItem[] {
44
+ return [
45
+ {
46
+ id: "preset",
47
+ label: "Preset profile",
48
+ description: "opencode = strict inline-only, balanced = compact summaries, verbose = line previews",
49
+ currentValue: detectToolDisplayPreset(config),
50
+ values: [...TOOL_DISPLAY_PRESETS],
51
+ },
52
+ {
53
+ id: "readOutputMode",
54
+ label: "Read tool output",
55
+ description: "hidden = OpenCode style (path only), summary = line count, preview = show file lines",
56
+ currentValue: config.readOutputMode,
57
+ values: ["hidden", "summary", "preview"],
58
+ },
59
+ {
60
+ id: "searchOutputMode",
61
+ label: "Grep/Find/Ls output",
62
+ description: "hidden = call only, count = match count, preview = show lines",
63
+ currentValue: config.searchOutputMode,
64
+ values: ["hidden", "count", "preview"],
65
+ },
66
+ {
67
+ id: "mcpOutputMode",
68
+ label: "MCP tool output",
69
+ description: "hidden = call only, summary = line count, preview = show lines",
70
+ currentValue: config.mcpOutputMode,
71
+ values: ["hidden", "summary", "preview"],
72
+ },
73
+ {
74
+ id: "previewLines",
75
+ label: "Preview lines (read/search/MCP)",
76
+ description: "Lines shown in collapsed mode when preview mode is enabled",
77
+ currentValue: String(config.previewLines),
78
+ values: PREVIEW_LINE_VALUES,
79
+ },
80
+ {
81
+ id: "expandedPreviewMaxLines",
82
+ label: "Expanded max lines (Ctrl+O)",
83
+ description: "Safety ceiling for expanded read/search/MCP output rendering",
84
+ currentValue: String(config.expandedPreviewMaxLines),
85
+ values: EXPANDED_PREVIEW_MAX_LINE_VALUES,
86
+ },
87
+ {
88
+ id: "bashCollapsedLines",
89
+ label: "Bash collapsed lines",
90
+ description: "OpenCode default is 10; set 0 to hide bash output when collapsed",
91
+ currentValue: String(config.bashCollapsedLines),
92
+ values: BASH_PREVIEW_LINE_VALUES,
93
+ },
94
+ {
95
+ id: "diffViewMode",
96
+ label: "Edit diff layout",
97
+ description: "auto = adaptive, split = force side-by-side, unified = force single-column",
98
+ currentValue: config.diffViewMode,
99
+ values: ["auto", "split", "unified"],
100
+ },
101
+ {
102
+ id: "diffSplitMinWidth",
103
+ label: "Diff split min width",
104
+ description: "Minimum terminal width required before side-by-side diff is used",
105
+ currentValue: String(config.diffSplitMinWidth),
106
+ values: DIFF_SPLIT_MIN_WIDTH_VALUES,
107
+ },
108
+ {
109
+ id: "diffCollapsedLines",
110
+ label: "Diff collapsed lines",
111
+ description: "Maximum diff lines shown before expand (Ctrl+O)",
112
+ currentValue: String(config.diffCollapsedLines),
113
+ values: DIFF_COLLAPSED_LINE_VALUES,
114
+ },
115
+ {
116
+ id: "diffWordWrap",
117
+ label: "Diff word wrap",
118
+ description: "Wrap long diff lines instead of clipping them",
119
+ currentValue: toOnOff(config.diffWordWrap),
120
+ values: ["on", "off"],
121
+ },
122
+ {
123
+ id: "showTruncationHints",
124
+ label: "Show truncation hints",
125
+ description: "Shows notices when backend line/byte truncation happens",
126
+ currentValue: toOnOff(config.showTruncationHints),
127
+ values: ["on", "off"],
128
+ },
129
+ {
130
+ id: "showRtkCompactionHints",
131
+ label: "Show RTK compaction hints",
132
+ description: "Shows RTK compaction labels (including summary suffix text)",
133
+ currentValue: toOnOff(config.showRtkCompactionHints),
134
+ values: ["on", "off"],
135
+ },
136
+ ];
137
+ }
138
+
139
+ function applyPreset(preset: ToolDisplayPreset): ToolDisplayConfig {
140
+ return getToolDisplayPresetConfig(preset);
141
+ }
142
+
143
+ function applySetting(config: ToolDisplayConfig, id: string, value: string): ToolDisplayConfig {
144
+ switch (id) {
145
+ case "preset": {
146
+ const parsed = parseToolDisplayPreset(value);
147
+ return parsed ? applyPreset(parsed) : config;
148
+ }
149
+ case "readOutputMode":
150
+ return {
151
+ ...config,
152
+ readOutputMode: value as ToolDisplayConfig["readOutputMode"],
153
+ };
154
+ case "searchOutputMode":
155
+ return {
156
+ ...config,
157
+ searchOutputMode: value as ToolDisplayConfig["searchOutputMode"],
158
+ };
159
+ case "mcpOutputMode":
160
+ return {
161
+ ...config,
162
+ mcpOutputMode: value as ToolDisplayConfig["mcpOutputMode"],
163
+ };
164
+ case "previewLines":
165
+ return {
166
+ ...config,
167
+ previewLines: parseNumber(value, config.previewLines),
168
+ };
169
+ case "expandedPreviewMaxLines":
170
+ return {
171
+ ...config,
172
+ expandedPreviewMaxLines: parseNumber(value, config.expandedPreviewMaxLines),
173
+ };
174
+ case "bashCollapsedLines":
175
+ return {
176
+ ...config,
177
+ bashCollapsedLines: parseNumber(value, config.bashCollapsedLines),
178
+ };
179
+ case "diffViewMode":
180
+ return {
181
+ ...config,
182
+ diffViewMode: value as ToolDisplayConfig["diffViewMode"],
183
+ };
184
+ case "diffSplitMinWidth":
185
+ return {
186
+ ...config,
187
+ diffSplitMinWidth: parseNumber(value, config.diffSplitMinWidth),
188
+ };
189
+ case "diffCollapsedLines":
190
+ return {
191
+ ...config,
192
+ diffCollapsedLines: parseNumber(value, config.diffCollapsedLines),
193
+ };
194
+ case "diffWordWrap":
195
+ return {
196
+ ...config,
197
+ diffWordWrap: value === "on",
198
+ };
199
+ case "showTruncationHints":
200
+ return {
201
+ ...config,
202
+ showTruncationHints: value === "on",
203
+ };
204
+ case "showRtkCompactionHints":
205
+ return {
206
+ ...config,
207
+ showRtkCompactionHints: value === "on",
208
+ };
209
+ default:
210
+ return config;
211
+ }
212
+ }
213
+
214
+ function syncSettingValues(settingsList: SettingValueSyncTarget, config: ToolDisplayConfig): void {
215
+ settingsList.updateValue("preset", detectToolDisplayPreset(config));
216
+ settingsList.updateValue("readOutputMode", config.readOutputMode);
217
+ settingsList.updateValue("searchOutputMode", config.searchOutputMode);
218
+ settingsList.updateValue("mcpOutputMode", config.mcpOutputMode);
219
+ settingsList.updateValue("previewLines", String(config.previewLines));
220
+ settingsList.updateValue("expandedPreviewMaxLines", String(config.expandedPreviewMaxLines));
221
+ settingsList.updateValue("bashCollapsedLines", String(config.bashCollapsedLines));
222
+ settingsList.updateValue("diffViewMode", config.diffViewMode);
223
+ settingsList.updateValue("diffSplitMinWidth", String(config.diffSplitMinWidth));
224
+ settingsList.updateValue("diffCollapsedLines", String(config.diffCollapsedLines));
225
+ settingsList.updateValue("diffWordWrap", toOnOff(config.diffWordWrap));
226
+ settingsList.updateValue("showTruncationHints", toOnOff(config.showTruncationHints));
227
+ settingsList.updateValue("showRtkCompactionHints", toOnOff(config.showRtkCompactionHints));
228
+ }
229
+
230
+ async function openSettingsModal(ctx: ExtensionCommandContext, controller: ToolDisplayConfigController): Promise<void> {
231
+ const overlayOptions = { anchor: "center" as const, width: 76, maxHeight: "80%" as const, margin: 1 };
232
+
233
+ await ctx.ui.custom<void>(
234
+ (tui, theme, _keybindings, done) => {
235
+ let current = controller.getConfig();
236
+ let settingsModal: ZellijSettingsModal | null = null;
237
+
238
+ settingsModal = new ZellijSettingsModal(
239
+ {
240
+ title: "Tool Display Settings",
241
+ description: "OpenCode-style tool output behavior for pi",
242
+ settings: buildSettingItems(current),
243
+ onChange: (id, newValue) => {
244
+ current = applySetting(current, id, newValue);
245
+ controller.setConfig(current, ctx);
246
+ current = controller.getConfig();
247
+ if (settingsModal) {
248
+ syncSettingValues(settingsModal, current);
249
+ }
250
+ },
251
+ onClose: () => done(),
252
+ helpText: `/tool-display preset ${PRESET_COMMAND_HINT} • /tool-display show`,
253
+ enableSearch: true,
254
+ },
255
+ theme,
256
+ );
257
+
258
+ const modal = new ZellijModal(
259
+ settingsModal,
260
+ {
261
+ borderStyle: "rounded",
262
+ titleBar: {
263
+ left: "Tool Display Settings",
264
+ right: "pi-tool-display",
265
+ },
266
+ helpUndertitle: {
267
+ text: "Esc: close | ↑↓: navigate | Space: toggle",
268
+ color: "dim",
269
+ },
270
+ overlay: overlayOptions,
271
+ },
272
+ theme,
273
+ );
274
+
275
+ return {
276
+ render(width: number) {
277
+ return modal.renderModal(width).lines;
278
+ },
279
+ invalidate() {
280
+ modal.invalidate();
281
+ },
282
+ handleInput(data: string) {
283
+ modal.handleInput(data);
284
+ tui.requestRender();
285
+ },
286
+ };
287
+ },
288
+ { overlay: true, overlayOptions },
289
+ );
290
+ }
291
+
292
+ function handleToolDisplayArgs(args: string, ctx: ExtensionCommandContext, controller: ToolDisplayConfigController): boolean {
293
+ const raw = args.trim();
294
+ if (!raw) {
295
+ return false;
296
+ }
297
+
298
+ const normalized = raw.toLowerCase();
299
+
300
+ if (normalized === "show") {
301
+ ctx.ui.notify(`tool-display: ${summarizeConfig(controller.getConfig())}`, "info");
302
+ return true;
303
+ }
304
+
305
+ if (normalized === "reset") {
306
+ controller.setConfig(getToolDisplayPresetConfig("opencode"), ctx);
307
+ ctx.ui.notify("Tool display preset reset to opencode.", "info");
308
+ return true;
309
+ }
310
+
311
+ if (normalized.startsWith("preset ")) {
312
+ const candidate = normalized.slice("preset ".length).trim();
313
+ const preset = parseToolDisplayPreset(candidate);
314
+ if (!preset) {
315
+ ctx.ui.notify(`Unknown preset. Use: /tool-display preset ${PRESET_COMMAND_HINT}`, "warning");
316
+ return true;
317
+ }
318
+
319
+ controller.setConfig(getToolDisplayPresetConfig(preset), ctx);
320
+ ctx.ui.notify(`Tool display preset set to ${preset}.`, "info");
321
+ return true;
322
+ }
323
+
324
+
325
+ ctx.ui.notify(`Usage: /tool-display [show|reset|preset ${PRESET_COMMAND_HINT}]`, "warning");
326
+ return true;
327
+ }
328
+
329
+ export function registerToolDisplayCommand(pi: ExtensionAPI, controller: ToolDisplayConfigController): void {
330
+ pi.registerCommand("tool-display", {
331
+ description: "Configure tool output rendering (OpenCode-style)",
332
+ handler: async (args, ctx) => {
333
+ if (handleToolDisplayArgs(args, ctx, controller)) {
334
+ return;
335
+ }
336
+
337
+ if (!ctx.hasUI) {
338
+ ctx.ui.notify("/tool-display requires interactive TUI mode.", "warning");
339
+ return;
340
+ }
341
+
342
+ await openSettingsModal(ctx, controller);
343
+ },
344
+ });
345
+ }
@@ -0,0 +1,133 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ DEFAULT_TOOL_DISPLAY_CONFIG,
6
+ type ConfigLoadResult,
7
+ type ConfigSaveResult,
8
+ DIFF_VIEW_MODES,
9
+ MCP_OUTPUT_MODES,
10
+ READ_OUTPUT_MODES,
11
+ SEARCH_OUTPUT_MODES,
12
+ type ToolDisplayConfig,
13
+ } from "./types.js";
14
+
15
+ const CONFIG_DIR = join(homedir(), ".pi", "agent", "extensions", "pi-tool-display");
16
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
17
+
18
+ function clampNumber(value: unknown, min: number, max: number, fallback: number): number {
19
+ if (typeof value !== "number" || Number.isNaN(value)) {
20
+ return fallback;
21
+ }
22
+ const rounded = Math.floor(value);
23
+ if (rounded < min) return min;
24
+ if (rounded > max) return max;
25
+ return rounded;
26
+ }
27
+
28
+ function toBoolean(value: unknown, fallback: boolean): boolean {
29
+ return typeof value === "boolean" ? value : fallback;
30
+ }
31
+
32
+ function toReadOutputMode(value: unknown): ToolDisplayConfig["readOutputMode"] {
33
+ return READ_OUTPUT_MODES.includes(value as ToolDisplayConfig["readOutputMode"])
34
+ ? (value as ToolDisplayConfig["readOutputMode"])
35
+ : DEFAULT_TOOL_DISPLAY_CONFIG.readOutputMode;
36
+ }
37
+
38
+ function toSearchOutputMode(value: unknown): ToolDisplayConfig["searchOutputMode"] {
39
+ return SEARCH_OUTPUT_MODES.includes(value as ToolDisplayConfig["searchOutputMode"])
40
+ ? (value as ToolDisplayConfig["searchOutputMode"])
41
+ : DEFAULT_TOOL_DISPLAY_CONFIG.searchOutputMode;
42
+ }
43
+
44
+ function toMcpOutputMode(value: unknown): ToolDisplayConfig["mcpOutputMode"] {
45
+ return MCP_OUTPUT_MODES.includes(value as ToolDisplayConfig["mcpOutputMode"])
46
+ ? (value as ToolDisplayConfig["mcpOutputMode"])
47
+ : DEFAULT_TOOL_DISPLAY_CONFIG.mcpOutputMode;
48
+ }
49
+
50
+ function toDiffViewMode(value: unknown): ToolDisplayConfig["diffViewMode"] {
51
+ if (value === "stacked") {
52
+ // Backward compatibility with older config naming.
53
+ return "unified";
54
+ }
55
+
56
+ return DIFF_VIEW_MODES.includes(value as ToolDisplayConfig["diffViewMode"])
57
+ ? (value as ToolDisplayConfig["diffViewMode"])
58
+ : DEFAULT_TOOL_DISPLAY_CONFIG.diffViewMode;
59
+ }
60
+
61
+ export function normalizeToolDisplayConfig(raw: unknown): ToolDisplayConfig {
62
+ const source = typeof raw === "object" && raw !== null ? (raw as Partial<ToolDisplayConfig>) : {};
63
+
64
+ return {
65
+ readOutputMode: toReadOutputMode(source.readOutputMode),
66
+ searchOutputMode: toSearchOutputMode(source.searchOutputMode),
67
+ mcpOutputMode: toMcpOutputMode(source.mcpOutputMode),
68
+ previewLines: clampNumber(source.previewLines, 1, 80, DEFAULT_TOOL_DISPLAY_CONFIG.previewLines),
69
+ expandedPreviewMaxLines: clampNumber(
70
+ source.expandedPreviewMaxLines,
71
+ 0,
72
+ 20_000,
73
+ DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
74
+ ),
75
+ bashCollapsedLines: clampNumber(source.bashCollapsedLines, 0, 80, DEFAULT_TOOL_DISPLAY_CONFIG.bashCollapsedLines),
76
+ diffViewMode: toDiffViewMode(source.diffViewMode),
77
+ diffSplitMinWidth: clampNumber(source.diffSplitMinWidth, 70, 240, DEFAULT_TOOL_DISPLAY_CONFIG.diffSplitMinWidth),
78
+ diffCollapsedLines: clampNumber(source.diffCollapsedLines, 4, 240, DEFAULT_TOOL_DISPLAY_CONFIG.diffCollapsedLines),
79
+ diffWordWrap: toBoolean(source.diffWordWrap, DEFAULT_TOOL_DISPLAY_CONFIG.diffWordWrap),
80
+ showTruncationHints: toBoolean(source.showTruncationHints, DEFAULT_TOOL_DISPLAY_CONFIG.showTruncationHints),
81
+ showRtkCompactionHints: toBoolean(
82
+ source.showRtkCompactionHints,
83
+ DEFAULT_TOOL_DISPLAY_CONFIG.showRtkCompactionHints,
84
+ ),
85
+ };
86
+ }
87
+
88
+ export function loadToolDisplayConfig(): ConfigLoadResult {
89
+ if (!existsSync(CONFIG_FILE)) {
90
+ return { config: { ...DEFAULT_TOOL_DISPLAY_CONFIG } };
91
+ }
92
+
93
+ try {
94
+ const rawText = readFileSync(CONFIG_FILE, "utf-8");
95
+ const rawConfig = JSON.parse(rawText) as unknown;
96
+ return { config: normalizeToolDisplayConfig(rawConfig) };
97
+ } catch (error) {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ return {
100
+ config: { ...DEFAULT_TOOL_DISPLAY_CONFIG },
101
+ error: `Failed to parse ${CONFIG_FILE}: ${message}`,
102
+ };
103
+ }
104
+ }
105
+
106
+ export function saveToolDisplayConfig(config: ToolDisplayConfig): ConfigSaveResult {
107
+ const normalized = normalizeToolDisplayConfig(config);
108
+ const tmpFile = `${CONFIG_FILE}.tmp`;
109
+
110
+ try {
111
+ mkdirSync(CONFIG_DIR, { recursive: true });
112
+ writeFileSync(tmpFile, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
113
+ renameSync(tmpFile, CONFIG_FILE);
114
+ return { success: true };
115
+ } catch (error) {
116
+ try {
117
+ if (existsSync(tmpFile)) {
118
+ unlinkSync(tmpFile);
119
+ }
120
+ } catch {
121
+ // Ignore cleanup errors.
122
+ }
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ return {
125
+ success: false,
126
+ error: `Failed to save ${CONFIG_FILE}: ${message}`,
127
+ };
128
+ }
129
+ }
130
+
131
+ export function getToolDisplayConfigPath(): string {
132
+ return CONFIG_FILE;
133
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "readOutputMode": "hidden",
3
+ "searchOutputMode": "hidden",
4
+ "mcpOutputMode": "hidden",
5
+ "previewLines": 8,
6
+ "expandedPreviewMaxLines": 4000,
7
+ "bashCollapsedLines": 10,
8
+ "diffViewMode": "auto",
9
+ "diffSplitMinWidth": 120,
10
+ "diffCollapsedLines": 24,
11
+ "diffWordWrap": true,
12
+ "showTruncationHints": true,
13
+ "showRtkCompactionHints": true
14
+ }