@selesai/code 0.10.1 → 0.11.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/config.d.ts +5 -3
  3. package/dist/config.js +6 -4
  4. package/dist/core/skills.d.ts +2 -0
  5. package/dist/core/skills.js +1 -0
  6. package/dist/defaults/settings.json +0 -3
  7. package/dist/extensions/auto-session-name.test.ts +157 -0
  8. package/dist/extensions/auto-session-name.ts +96 -0
  9. package/dist/extensions/cost-reconcile.test.ts +76 -0
  10. package/dist/extensions/cost-reconcile.ts +278 -0
  11. package/dist/extensions/enable-readonly-tools.test.ts +11 -0
  12. package/dist/extensions/enable-readonly-tools.ts +7 -3
  13. package/dist/extensions/package.json +3 -0
  14. package/dist/extensions/pi-tool-display/CHANGELOG.md +329 -0
  15. package/dist/extensions/pi-tool-display/LICENSE +21 -0
  16. package/dist/extensions/pi-tool-display/README.md +417 -0
  17. package/dist/extensions/pi-tool-display/config/config.example.json +40 -0
  18. package/dist/extensions/pi-tool-display/index.ts +3 -0
  19. package/dist/extensions/pi-tool-display/package-lock.json +2555 -0
  20. package/dist/extensions/pi-tool-display/package.json +86 -0
  21. package/dist/extensions/pi-tool-display/src/agent-dir.ts +35 -0
  22. package/dist/extensions/pi-tool-display/src/ansi-utils.ts +116 -0
  23. package/dist/extensions/pi-tool-display/src/bash-display.ts +213 -0
  24. package/dist/extensions/pi-tool-display/src/capabilities.ts +88 -0
  25. package/dist/extensions/pi-tool-display/src/config-modal.ts +541 -0
  26. package/dist/extensions/pi-tool-display/src/config-store.ts +304 -0
  27. package/dist/extensions/pi-tool-display/src/debug-logger.ts +150 -0
  28. package/dist/extensions/pi-tool-display/src/diff-presentation.ts +72 -0
  29. package/dist/extensions/pi-tool-display/src/diff-renderer.ts +2517 -0
  30. package/dist/extensions/pi-tool-display/src/disposable.ts +34 -0
  31. package/dist/extensions/pi-tool-display/src/extension-lifecycle.ts +14 -0
  32. package/dist/extensions/pi-tool-display/src/index.ts +109 -0
  33. package/dist/extensions/pi-tool-display/src/line-width-safety.ts +93 -0
  34. package/dist/extensions/pi-tool-display/src/modal-icons.ts +57 -0
  35. package/dist/extensions/pi-tool-display/src/pending-diff-preview.ts +370 -0
  36. package/dist/extensions/pi-tool-display/src/presets.ts +117 -0
  37. package/dist/extensions/pi-tool-display/src/render-utils.ts +186 -0
  38. package/dist/extensions/pi-tool-display/src/settings-inspector-modal.ts +475 -0
  39. package/dist/extensions/pi-tool-display/src/thinking-label.ts +343 -0
  40. package/dist/extensions/pi-tool-display/src/tool-call-box.ts +181 -0
  41. package/dist/extensions/pi-tool-display/src/tool-metadata.ts +139 -0
  42. package/dist/extensions/pi-tool-display/src/tool-overrides.ts +2168 -0
  43. package/dist/extensions/pi-tool-display/src/types.ts +109 -0
  44. package/dist/extensions/pi-tool-display/src/user-message-box-markdown.ts +64 -0
  45. package/dist/extensions/pi-tool-display/src/user-message-box-native.ts +66 -0
  46. package/dist/extensions/pi-tool-display/src/user-message-box-patch.ts +72 -0
  47. package/dist/extensions/pi-tool-display/src/user-message-box-renderer.ts +419 -0
  48. package/dist/extensions/pi-tool-display/src/user-message-box-utils.ts +106 -0
  49. package/dist/extensions/pi-tool-display/src/write-display-utils.ts +31 -0
  50. package/dist/extensions/pi-tool-display/src/zellij-modal.ts +1027 -0
  51. package/dist/extensions/pi-tool-display/tests/active-backlog-red.test.ts +65 -0
  52. package/dist/extensions/pi-tool-display/tests/ansi-utils.test.ts +345 -0
  53. package/dist/extensions/pi-tool-display/tests/bash-display.test.ts +442 -0
  54. package/dist/extensions/pi-tool-display/tests/capabilities-edge.test.ts +360 -0
  55. package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +54 -0
  56. package/dist/extensions/pi-tool-display/tests/config-modal.test.ts +360 -0
  57. package/dist/extensions/pi-tool-display/tests/config-store.test.ts +87 -0
  58. package/dist/extensions/pi-tool-display/tests/custom-tool-overrides.test.ts +415 -0
  59. package/dist/extensions/pi-tool-display/tests/debug-logger-edge.test.ts +630 -0
  60. package/dist/extensions/pi-tool-display/tests/debug-logger.test.ts +62 -0
  61. package/dist/extensions/pi-tool-display/tests/diff-renderer-ansi.test.ts +55 -0
  62. package/dist/extensions/pi-tool-display/tests/diff-renderer-edge.test.ts +886 -0
  63. package/dist/extensions/pi-tool-display/tests/diff-renderer-numbering.test.ts +135 -0
  64. package/dist/extensions/pi-tool-display/tests/diff-renderer-width.test.ts +454 -0
  65. package/dist/extensions/pi-tool-display/tests/diff-renderer-wrap.test.ts +169 -0
  66. package/dist/extensions/pi-tool-display/tests/index-integration.test.ts +354 -0
  67. package/dist/extensions/pi-tool-display/tests/pending-diff-preview.test.ts +79 -0
  68. package/dist/extensions/pi-tool-display/tests/presets-edge.test.ts +229 -0
  69. package/dist/extensions/pi-tool-display/tests/presets.test.ts +26 -0
  70. package/dist/extensions/pi-tool-display/tests/reload-behavior.test.ts +1241 -0
  71. package/dist/extensions/pi-tool-display/tests/render-utils.test.ts +710 -0
  72. package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +450 -0
  73. package/dist/extensions/pi-tool-display/tests/thinking-label-edge.test.ts +904 -0
  74. package/dist/extensions/pi-tool-display/tests/thinking-label.test.ts +72 -0
  75. package/dist/extensions/pi-tool-display/tests/tool-call-box.test.ts +105 -0
  76. package/dist/extensions/pi-tool-display/tests/tool-overrides-config.test.ts +613 -0
  77. package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +499 -0
  78. package/dist/extensions/pi-tool-display/tests/tool-overrides-registration.test.ts +315 -0
  79. package/dist/extensions/pi-tool-display/tests/tool-overrides-write-meta.test.ts +68 -0
  80. package/dist/extensions/pi-tool-display/tests/tool-ui-utils.test.ts +413 -0
  81. package/dist/extensions/pi-tool-display/tests/user-message-box-performance-red.test.ts +176 -0
  82. package/dist/extensions/pi-tool-display/tests/user-message-box.test.ts +908 -0
  83. package/dist/extensions/pi-tool-display/tests/zellij-modal.test.ts +848 -0
  84. package/dist/extensions/pi-tool-display/tool-display-api-consumer.d.ts +30 -0
  85. package/dist/extensions/pi-tool-display/tool-display-api-consumer.js +40 -0
  86. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +46 -1
  87. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +10 -0
  88. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  89. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +3 -3
  90. package/dist/extensions/pi-zentui/test/format.test.ts +50 -2
  91. package/dist/modes/interactive/components/startup-box.d.ts +38 -0
  92. package/dist/modes/interactive/components/startup-box.js +90 -0
  93. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  94. package/dist/modes/interactive/interactive-mode.js +85 -122
  95. package/dist/skills/agent-browser/SKILL.md +1 -0
  96. package/dist/skills/batch-grill-me/SKILL.md +1 -0
  97. package/dist/skills/brandkit/SKILL.md +1 -0
  98. package/dist/skills/design-references/SKILL.md +1 -0
  99. package/dist/skills/design-taste-frontend/SKILL.md +1 -0
  100. package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
  101. package/dist/skills/full-output-enforcement/SKILL.md +1 -0
  102. package/dist/skills/gpt-taste/SKILL.md +1 -0
  103. package/dist/skills/grill-me/SKILL.md +1 -0
  104. package/dist/skills/handoff/SKILL.md +1 -0
  105. package/dist/skills/handoff-text/SKILL.md +1 -0
  106. package/dist/skills/high-end-visual-design/SKILL.md +1 -0
  107. package/dist/skills/image-to-code/SKILL.md +1 -0
  108. package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
  109. package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
  110. package/dist/skills/implanger/SKILL.md +1 -0
  111. package/dist/skills/improve-codebase/SKILL.md +1 -0
  112. package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
  113. package/dist/skills/minimalist-ui/SKILL.md +1 -0
  114. package/dist/skills/pi-subagents/SKILL.md +1 -0
  115. package/dist/skills/planger/SKILL.md +1 -0
  116. package/dist/skills/ponytail/SKILL.md +1 -0
  117. package/dist/skills/ponytail-audit/SKILL.md +1 -0
  118. package/dist/skills/ponytail-debt/SKILL.md +1 -0
  119. package/dist/skills/ponytail-gain/SKILL.md +1 -0
  120. package/dist/skills/ponytail-help/SKILL.md +1 -0
  121. package/dist/skills/ponytail-review/SKILL.md +1 -0
  122. package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
  123. package/dist/skills/selesai-handoff/SKILL.md +1 -0
  124. package/dist/skills/stitch-design-taste/SKILL.md +1 -0
  125. package/dist/skills/unlazy/SKILL.md +1 -0
  126. package/dist/skills/web-design-guidelines/SKILL.md +1 -0
  127. package/dist/skills/workflow/SKILL.md +1 -0
  128. package/package.json +3 -3
@@ -0,0 +1,360 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
+ import { registerToolDisplayCommand } from "../src/config-modal.ts";
5
+ import {
6
+ DEFAULT_TOOL_DISPLAY_CONFIG,
7
+ type ToolDisplayConfig,
8
+ } from "../src/types.ts";
9
+ import type { ToolDisplayCapabilities } from "../src/capabilities.ts";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Stubs
13
+ // ---------------------------------------------------------------------------
14
+
15
+ interface Notification {
16
+ message: string;
17
+ level: string;
18
+ }
19
+
20
+ function createPiStub(): {
21
+ api: ExtensionAPI;
22
+ getHandler: () => ((args: string, ctx: ExtensionCommandContext) => Promise<void>) | undefined;
23
+ } {
24
+ let handler: ((args: string, ctx: ExtensionCommandContext) => Promise<void>) | undefined;
25
+ const api = {
26
+ registerCommand(_name: string, cmd: { description: string; handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> }) {
27
+ handler = cmd.handler;
28
+ },
29
+ } as unknown as ExtensionAPI;
30
+ return {
31
+ api,
32
+ getHandler: () => handler,
33
+ };
34
+ }
35
+
36
+ function createCtxStub(
37
+ hasUI: boolean,
38
+ customFn?: () => Promise<void>,
39
+ ): {
40
+ ctx: ExtensionCommandContext;
41
+ notifications: Notification[];
42
+ } {
43
+ const notifications: Notification[] = [];
44
+ return {
45
+ ctx: {
46
+ hasUI,
47
+ ui: {
48
+ notify: (message: string, level: string): void => {
49
+ notifications.push({ message, level });
50
+ },
51
+ custom: customFn ?? (async (): Promise<void> => {}),
52
+ },
53
+ } as unknown as ExtensionCommandContext,
54
+ notifications,
55
+ };
56
+ }
57
+
58
+ function createControllerStub(
59
+ initialConfig?: Partial<ToolDisplayConfig>,
60
+ capabilities?: ToolDisplayCapabilities,
61
+ ): {
62
+ controller: {
63
+ getConfig: () => ToolDisplayConfig;
64
+ setConfig: (next: ToolDisplayConfig, ctx: ExtensionCommandContext) => void;
65
+ getCapabilities: () => ToolDisplayCapabilities;
66
+ };
67
+ getLastSet: () => { config: ToolDisplayConfig | null; ctx: ExtensionCommandContext | null };
68
+ } {
69
+ let config: ToolDisplayConfig = {
70
+ ...DEFAULT_TOOL_DISPLAY_CONFIG,
71
+ ...initialConfig,
72
+ registerToolOverrides: {
73
+ ...(initialConfig?.registerToolOverrides ?? DEFAULT_TOOL_DISPLAY_CONFIG.registerToolOverrides),
74
+ },
75
+ };
76
+ const last = { config: null as ToolDisplayConfig | null, ctx: null as ExtensionCommandContext | null };
77
+
78
+ return {
79
+ controller: {
80
+ getConfig: () => ({
81
+ ...config,
82
+ registerToolOverrides: { ...config.registerToolOverrides },
83
+ }),
84
+ setConfig: (next: ToolDisplayConfig, ctx: ExtensionCommandContext) => {
85
+ config = { ...next, registerToolOverrides: { ...next.registerToolOverrides } };
86
+ last.config = config;
87
+ last.ctx = ctx;
88
+ },
89
+ getCapabilities: () =>
90
+ capabilities ?? { hasMcpTooling: false, hasRtkOptimizer: false },
91
+ },
92
+ getLastSet: () => last,
93
+ };
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Tests
98
+ // ---------------------------------------------------------------------------
99
+
100
+ test("registerToolDisplayCommand registers a handler for 'tool-display'", () => {
101
+ const { api, getHandler } = createPiStub();
102
+ const { controller } = createControllerStub();
103
+
104
+ registerToolDisplayCommand(api, controller);
105
+
106
+ assert.ok(getHandler(), "expected handler to be registered");
107
+ });
108
+
109
+ test("'show' argument notifies with config summary", async () => {
110
+ const { api, getHandler } = createPiStub();
111
+ const { controller } = createControllerStub({}, { hasMcpTooling: true, hasRtkOptimizer: true });
112
+ const { ctx, notifications } = createCtxStub(true);
113
+
114
+ registerToolDisplayCommand(api, controller);
115
+ const handler = getHandler();
116
+ assert.ok(handler);
117
+
118
+ await handler("show", ctx);
119
+
120
+ assert.equal(notifications.length, 1);
121
+ assert.match(notifications[0]?.message ?? "", /^tool-display: /);
122
+ assert.ok(notifications[0]?.message.includes("preset=boxed"));
123
+ assert.ok(notifications[0]?.message.includes("mcp=hidden"), "MCP setting in summary with MCP capability");
124
+ assert.ok(
125
+ notifications[0]?.message.includes("rtkHints=off"),
126
+ "RTK hints in summary with RTK capability",
127
+ );
128
+ assert.equal(notifications[0]?.level, "info");
129
+ });
130
+
131
+ test("'show' hides MCP and RTK sections when capabilities absent", async () => {
132
+ const { api, getHandler } = createPiStub();
133
+ const { controller } = createControllerStub();
134
+ const { ctx, notifications } = createCtxStub(true);
135
+
136
+ registerToolDisplayCommand(api, controller);
137
+ const handler = getHandler();
138
+ assert.ok(handler);
139
+
140
+ await handler("show", ctx);
141
+
142
+ assert.equal(notifications.length, 1);
143
+ assert.ok(notifications[0]?.message.includes("mcp=auto-hidden"));
144
+ assert.ok(notifications[0]?.message.includes("rtkHints=auto-off"));
145
+ });
146
+
147
+ test("'reset' argument sets config to boxed default preset", async () => {
148
+ const { api, getHandler } = createPiStub();
149
+ const { controller, getLastSet } = createControllerStub({
150
+ readOutputMode: "preview",
151
+ searchOutputMode: "preview",
152
+ previewLines: 99,
153
+ });
154
+ const { ctx, notifications } = createCtxStub(true);
155
+
156
+ registerToolDisplayCommand(api, controller);
157
+ const handler = getHandler();
158
+ assert.ok(handler);
159
+
160
+ await handler("reset", ctx);
161
+
162
+ const last = getLastSet();
163
+ assert.ok(last.config, "expected setConfig to be called");
164
+ assert.equal(last.config!.readOutputMode, DEFAULT_TOOL_DISPLAY_CONFIG.readOutputMode);
165
+ assert.equal(last.config!.searchOutputMode, DEFAULT_TOOL_DISPLAY_CONFIG.searchOutputMode);
166
+ assert.equal(last.config!.previewLines, DEFAULT_TOOL_DISPLAY_CONFIG.previewLines);
167
+ assert.equal(last.config!.bashOutputMode, DEFAULT_TOOL_DISPLAY_CONFIG.bashOutputMode);
168
+ assert.equal(last.config!.boxedToolCalls, true);
169
+ assert.match(notifications[0]?.message ?? "", /reset to boxed/i);
170
+ assert.equal(notifications[0]?.level, "info");
171
+ });
172
+
173
+ test("'preset balanced' sets correct config", async () => {
174
+ const { api, getHandler } = createPiStub();
175
+ const { controller, getLastSet } = createControllerStub();
176
+ const { ctx, notifications } = createCtxStub(true);
177
+
178
+ registerToolDisplayCommand(api, controller);
179
+ const handler = getHandler();
180
+ assert.ok(handler);
181
+
182
+ await handler("preset balanced", ctx);
183
+
184
+ const last = getLastSet();
185
+ assert.ok(last.config);
186
+ assert.equal(last.config!.readOutputMode, "summary");
187
+ assert.equal(last.config!.searchOutputMode, "count");
188
+ assert.equal(last.config!.mcpOutputMode, "summary");
189
+ assert.equal(last.config!.bashOutputMode, "summary");
190
+ assert.match(notifications[0]?.message ?? "", /set to balanced/i);
191
+ });
192
+
193
+ test("'preset verbose' sets correct config", async () => {
194
+ const { api, getHandler } = createPiStub();
195
+ const { controller, getLastSet } = createControllerStub();
196
+ const { ctx } = createCtxStub(true);
197
+
198
+ registerToolDisplayCommand(api, controller);
199
+ const handler = getHandler();
200
+ assert.ok(handler);
201
+
202
+ await handler("preset verbose", ctx);
203
+
204
+ const last = getLastSet();
205
+ assert.ok(last.config);
206
+ assert.equal(last.config!.readOutputMode, "preview");
207
+ assert.equal(last.config!.searchOutputMode, "preview");
208
+ assert.equal(last.config!.mcpOutputMode, "preview");
209
+ assert.equal(last.config!.previewLines, 12);
210
+ assert.equal(last.config!.bashCollapsedLines, 20);
211
+ });
212
+
213
+ test("'preset <invalid>' warns about unknown preset", async () => {
214
+ const { api, getHandler } = createPiStub();
215
+ const { controller, getLastSet } = createControllerStub();
216
+ const { ctx, notifications } = createCtxStub(true);
217
+
218
+ registerToolDisplayCommand(api, controller);
219
+ const handler = getHandler();
220
+ assert.ok(handler);
221
+
222
+ await handler("preset turbo", ctx);
223
+
224
+ assert.equal(getLastSet().config, null, "setConfig should not be called");
225
+ assert.equal(notifications.length, 1);
226
+ assert.match(notifications[0]?.message ?? "", /unknown preset/i);
227
+ assert.equal(notifications[0]?.level, "warning");
228
+ });
229
+
230
+ test("'preset' alone (no name) warns about unknown preset", async () => {
231
+ const { api, getHandler } = createPiStub();
232
+ const { controller, getLastSet } = createControllerStub();
233
+ const { ctx, notifications } = createCtxStub(true);
234
+
235
+ registerToolDisplayCommand(api, controller);
236
+ const handler = getHandler();
237
+ assert.ok(handler);
238
+
239
+ await handler("preset ", ctx);
240
+
241
+ assert.equal(getLastSet().config, null, "setConfig should not be called for empty preset name");
242
+ assert.ok(notifications.length >= 1);
243
+ });
244
+
245
+ test("empty args with TUI mode opens modal via ctx.ui.custom", async () => {
246
+ const { api, getHandler } = createPiStub();
247
+ const { controller } = createControllerStub();
248
+ let customCalled = false;
249
+ const { ctx, notifications } = createCtxStub(true, async () => {
250
+ customCalled = true;
251
+ });
252
+
253
+ registerToolDisplayCommand(api, controller);
254
+ const handler = getHandler();
255
+ assert.ok(handler);
256
+
257
+ await handler("", ctx);
258
+
259
+ assert.ok(customCalled, "expected ctx.ui.custom() to be called");
260
+ // Should be no notification since we go to modal
261
+ assert.equal(notifications.length, 0);
262
+ });
263
+
264
+ test("empty args without TUI mode warns about TUI requirement", async () => {
265
+ const { api, getHandler } = createPiStub();
266
+ const { controller } = createControllerStub();
267
+ const { ctx, notifications } = createCtxStub(false);
268
+
269
+ registerToolDisplayCommand(api, controller);
270
+ const handler = getHandler();
271
+ assert.ok(handler);
272
+
273
+ await handler("", ctx);
274
+
275
+ assert.equal(notifications.length, 1);
276
+ assert.match(notifications[0]?.message ?? "", /interactive TUI mode/i);
277
+ assert.equal(notifications[0]?.level, "warning");
278
+ });
279
+
280
+ test("unknown command shows usage hint", async () => {
281
+ const { api, getHandler } = createPiStub();
282
+ const { controller, getLastSet } = createControllerStub();
283
+ const { ctx, notifications } = createCtxStub(true);
284
+
285
+ registerToolDisplayCommand(api, controller);
286
+ const handler = getHandler();
287
+ assert.ok(handler);
288
+
289
+ await handler("foobar", ctx);
290
+
291
+ assert.equal(getLastSet().config, null, "setConfig should not be called");
292
+ assert.equal(notifications.length, 1);
293
+ assert.match(notifications[0]?.message ?? "", /usage/i);
294
+ assert.equal(notifications[0]?.level, "warning");
295
+ });
296
+
297
+ test("whitespace-only args is treated as empty (no TUI path)", async () => {
298
+ const { api, getHandler } = createPiStub();
299
+ const { controller } = createControllerStub();
300
+ const { ctx, notifications } = createCtxStub(false);
301
+
302
+ registerToolDisplayCommand(api, controller);
303
+ const handler = getHandler();
304
+ assert.ok(handler);
305
+
306
+ await handler(" ", ctx);
307
+
308
+ assert.equal(notifications.length, 1);
309
+ assert.match(notifications[0]?.message ?? "", /interactive TUI mode/i);
310
+ });
311
+
312
+ test("'preset OPencode ' is case and whitespace insensitive", async () => {
313
+ const { api, getHandler } = createPiStub();
314
+ const { controller, getLastSet } = createControllerStub();
315
+ const { ctx } = createCtxStub(true);
316
+
317
+ registerToolDisplayCommand(api, controller);
318
+ const handler = getHandler();
319
+ assert.ok(handler);
320
+
321
+ await handler("preset OPencode ", ctx);
322
+
323
+ const last = getLastSet();
324
+ assert.ok(last.config, "expected opencode preset to be applied");
325
+ assert.equal(last.config!.readOutputMode, DEFAULT_TOOL_DISPLAY_CONFIG.readOutputMode);
326
+ });
327
+
328
+ test("'preset Balanced' (capitalised) applies the balanced preset", async () => {
329
+ const { api, getHandler } = createPiStub();
330
+ const { controller, getLastSet } = createControllerStub();
331
+ const { ctx } = createCtxStub(true);
332
+
333
+ registerToolDisplayCommand(api, controller);
334
+ const handler = getHandler();
335
+ assert.ok(handler);
336
+
337
+ await handler("preset Balanced", ctx);
338
+
339
+ const last = getLastSet();
340
+ assert.ok(last.config);
341
+ assert.equal(last.config!.searchOutputMode, "count");
342
+ });
343
+
344
+ test("handler propagates rejection when ctx.ui.custom rejects", async () => {
345
+ const { api, getHandler } = createPiStub();
346
+ const { controller } = createControllerStub();
347
+ const { ctx } = createCtxStub(true, async () => {
348
+ throw new Error("modal rejected");
349
+ });
350
+
351
+ registerToolDisplayCommand(api, controller);
352
+ const handler = getHandler();
353
+ assert.ok(handler);
354
+
355
+ // The handler awaits openSettingsModal which calls ctx.ui.custom.
356
+ // When custom rejects, the handler propagates the rejection.
357
+ await assert.rejects(async () => {
358
+ await handler("", ctx);
359
+ }, /modal rejected/);
360
+ });
@@ -0,0 +1,87 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ loadToolDisplayConfig,
8
+ normalizeToolDisplayConfig,
9
+ saveToolDisplayConfig,
10
+ } from "../src/config-store.ts";
11
+ import { DEFAULT_TOOL_DISPLAY_CONFIG } from "../src/types.ts";
12
+
13
+ function withTempDir(name: string, run: (dir: string) => void): void {
14
+ const dir = mkdtempSync(join(tmpdir(), name));
15
+ try {
16
+ run(dir);
17
+ } finally {
18
+ rmSync(dir, { recursive: true, force: true });
19
+ }
20
+ }
21
+
22
+ test("config normalization clamps invalid values and migrates legacy read override", () => {
23
+ const config = normalizeToolDisplayConfig({
24
+ registerReadToolOverride: false,
25
+ registerToolOverrides: { bash: false },
26
+ readOutputMode: "invalid",
27
+ searchOutputMode: "count",
28
+ mcpOutputMode: "preview",
29
+ previewLines: 999,
30
+ expandedPreviewMaxLines: -1,
31
+ bashCollapsedLines: 999,
32
+ diffViewMode: "stacked",
33
+ diffSplitMinWidth: 1,
34
+ diffCollapsedLines: 999,
35
+ diffWordWrap: false,
36
+ });
37
+
38
+ assert.equal(config.registerToolOverrides.read, false);
39
+ assert.equal(config.registerToolOverrides.grep, true);
40
+ assert.equal(config.registerToolOverrides.bash, false);
41
+ assert.equal(config.readOutputMode, DEFAULT_TOOL_DISPLAY_CONFIG.readOutputMode);
42
+ assert.equal(config.searchOutputMode, "count");
43
+ assert.equal(config.mcpOutputMode, "preview");
44
+ assert.equal(config.previewLines, 80);
45
+ assert.equal(config.expandedPreviewMaxLines, 0);
46
+ assert.equal(config.bashCollapsedLines, 80);
47
+ assert.equal(config.diffViewMode, "unified");
48
+ assert.equal(config.diffSplitMinWidth, 70);
49
+ assert.equal(config.diffCollapsedLines, 240);
50
+ assert.equal(config.diffWordWrap, false);
51
+ });
52
+
53
+ test("config load reports parse errors and falls back to defaults", () => {
54
+ withTempDir("pi-tool-display-config-load-", (dir) => {
55
+ const configFile = join(dir, "config.json");
56
+ writeFileSync(configFile, "{not-json", "utf8");
57
+
58
+ const result = loadToolDisplayConfig(configFile);
59
+
60
+ assert.deepEqual(result.config, DEFAULT_TOOL_DISPLAY_CONFIG);
61
+ assert.match(result.error ?? "", /Failed to parse/);
62
+ assert.match(result.error ?? "", /config\.json/);
63
+ });
64
+ });
65
+
66
+ test("config save writes normalized JSON and cleans temporary file on failure", () => {
67
+ withTempDir("pi-tool-display-config-save-", (dir) => {
68
+ const configFile = join(dir, "config.json");
69
+ const saved = saveToolDisplayConfig(
70
+ { ...DEFAULT_TOOL_DISPLAY_CONFIG, previewLines: 999 },
71
+ configFile,
72
+ );
73
+
74
+ assert.equal(saved.success, true);
75
+ const persisted = JSON.parse(readFileSync(configFile, "utf8")) as { previewLines?: number };
76
+ assert.equal(persisted.previewLines, 80);
77
+
78
+ const parentFile = join(dir, "not-a-directory");
79
+ writeFileSync(parentFile, "blocks mkdir", "utf8");
80
+ const blockedConfigFile = join(parentFile, "config.json");
81
+ const failed = saveToolDisplayConfig(DEFAULT_TOOL_DISPLAY_CONFIG, blockedConfigFile);
82
+
83
+ assert.equal(failed.success, false);
84
+ assert.match(failed.error ?? "", /Failed to save/);
85
+ assert.equal(existsSync(`${blockedConfigFile}.tmp`), false);
86
+ });
87
+ });