pi-rtk-optimizer 0.6.0 → 0.7.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.
@@ -1,118 +1,187 @@
1
- import assert from "node:assert/strict";
2
-
3
- import { computeRewriteDecision } from "./command-rewriter.ts";
4
- import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
-
7
- function createMockPi(execResult: { code: number; stdout?: string; stderr?: string }): ExtensionAPI {
8
- return { exec: async () => execResult } as unknown as ExtensionAPI;
9
- }
10
-
11
- runTest("empty command unchanged", async () => {
12
- const config = cloneDefaultConfig();
13
- const decision = await computeRewriteDecision("", config, createMockPi({ code: 1 }));
14
- assert.equal(decision.changed, false);
15
- assert.equal(decision.reason, "empty");
16
- });
17
-
18
- runTest("already rtk unchanged", async () => {
19
- const config = cloneDefaultConfig();
20
- const decision = await computeRewriteDecision("rtk status", config, createMockPi({ code: 1 }));
21
- assert.equal(decision.changed, false);
22
- assert.equal(decision.reason, "already_rtk");
23
- });
24
-
25
- runTest("rtk unsupported heredoc result leaves command unchanged", async () => {
26
- const config = cloneDefaultConfig();
27
- const decision = await computeRewriteDecision("cat <<EOF", config, createMockPi({ code: 1 }));
28
- assert.equal(decision.changed, false);
29
- assert.equal(decision.reason, "no_match");
30
- });
31
-
32
- runTest("quoted heredoc marker is delegated to RTK rewrite", async () => {
33
- const config = cloneDefaultConfig();
34
- const command = 'echo "<<not heredoc" && git status';
35
- const decision = await computeRewriteDecision(
36
- command,
37
- config,
38
- createMockPi({ code: 3, stdout: 'echo "<<not heredoc" && rtk git status' }),
39
- );
40
- assert.equal(decision.changed, true);
41
- assert.equal(decision.rewrittenCommand, 'echo "<<not heredoc" && rtk git status');
42
- assert.equal(decision.reason, "ok");
43
- });
44
-
45
- runTest("legacy category toggles do not pre-filter RTK rewrite source of truth", async () => {
46
- const config = { ...cloneDefaultConfig(), rewriteGitGithub: false };
47
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
48
- assert.equal(decision.changed, true);
49
- assert.equal(decision.rewrittenCommand, "rtk git status");
50
- assert.equal(decision.reason, "ok");
51
- });
52
-
53
- runTest("rtk exit 0 rewrites", async () => {
54
- const config = cloneDefaultConfig();
55
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 0, stdout: "rtk git status" }));
56
- assert.equal(decision.changed, true);
57
- assert.equal(decision.rewrittenCommand, "rtk git status");
58
- assert.equal(decision.reason, "ok");
59
- });
60
-
61
- runTest("rtk exit 3 rewrites", async () => {
62
- const config = cloneDefaultConfig();
63
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
64
- assert.equal(decision.changed, true);
65
- assert.equal(decision.rewrittenCommand, "rtk git status");
66
- assert.equal(decision.reason, "ok");
67
- });
68
-
69
- runTest("exit 1 leaves unchanged", async () => {
70
- const config = cloneDefaultConfig();
71
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 1 }));
72
- assert.equal(decision.changed, false);
73
- assert.equal(decision.reason, "no_match");
74
- });
75
-
76
- runTest("exit 2 leaves unchanged", async () => {
77
- const config = cloneDefaultConfig();
78
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 2, stderr: "denied" }));
79
- assert.equal(decision.changed, false);
80
- assert.equal(decision.reason, "no_match");
81
- });
82
-
83
- runTest("unknown category passes through to RTK", async () => {
84
- const config = cloneDefaultConfig();
85
- const pi = createMockPi({ code: 0, stdout: "rtk custom" });
86
- const decision = await computeRewriteDecision("custom-cmd", config, pi);
87
- assert.equal(decision.changed, true);
88
- assert.equal(decision.rewrittenCommand, "rtk custom");
89
- assert.equal(decision.reason, "ok");
90
- });
91
-
92
- runTest("exec error/timeout leaves unchanged", async () => {
93
- const config = cloneDefaultConfig();
94
- const pi = {
95
- exec: async () => {
96
- throw new Error("timeout");
97
- },
98
- } as unknown as ExtensionAPI;
99
- const decision = await computeRewriteDecision("git status", config, pi);
100
- assert.equal(decision.changed, false);
101
- assert.equal(decision.reason, "no_match");
102
- });
103
-
104
- runTest("compound commands forwarded to RTK", async () => {
105
- const config = cloneDefaultConfig();
106
- let capturedArgs: string[] = [];
107
- const pi = {
108
- exec: async (_cmd: string, args: string[]) => {
109
- capturedArgs = args;
110
- return { code: 0, stdout: "rtk result" };
111
- },
112
- } as unknown as ExtensionAPI;
113
- const decision = await computeRewriteDecision("git status && cargo test", config, pi);
114
- assert.equal(decision.changed, true);
115
- assert.deepEqual(capturedArgs, ["rewrite", "git status && cargo test"]);
116
- });
117
-
118
- console.log("All command-rewriter tests passed.");
1
+ import assert from "node:assert/strict";
2
+
3
+ import { computeRewriteDecision } from "./command-rewriter.ts";
4
+ import { resolveRtkRewrite } from "./rtk-rewrite-provider.ts";
5
+ import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
6
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+
8
+ function createMockPi(execResult: { code: number; stdout?: string; stderr?: string }): ExtensionAPI {
9
+ return {
10
+ exec: async (command: string) => {
11
+ if (command === "which" || command === "where") {
12
+ return { code: 0, stdout: "/usr/local/bin/rtk\n", stderr: "" };
13
+ }
14
+ return execResult;
15
+ },
16
+ } as unknown as ExtensionAPI;
17
+ }
18
+
19
+ await runTest("rtk rewrite uses resolved POSIX executable path", async () => {
20
+ const calls: Array<{ command: string; args: string[] }> = [];
21
+ const pi = {
22
+ exec: async (command: string, args: string[]) => {
23
+ calls.push({ command, args });
24
+ if (command === "which") {
25
+ return { code: 0, stdout: "/opt/rtk/bin/rtk\n", stderr: "" };
26
+ }
27
+ return { code: 3, stdout: "rtk git status", stderr: "" };
28
+ },
29
+ } as unknown as ExtensionAPI;
30
+
31
+ const result = await resolveRtkRewrite(pi, "git status", { platform: "linux" });
32
+
33
+ assert.equal(result.changed, true);
34
+ assert.equal(result.rewrittenCommand, "rtk git status");
35
+ assert.equal(result.executableResolution?.resolvedPath, "/opt/rtk/bin/rtk");
36
+ assert.deepEqual(calls.map((call) => call.command), ["which", "/opt/rtk/bin/rtk"]);
37
+ });
38
+
39
+ await runTest("rtk rewrite uses resolved Windows executable path", async () => {
40
+ const calls: Array<{ command: string; args: string[] }> = [];
41
+ const pi = {
42
+ exec: async (command: string, args: string[]) => {
43
+ calls.push({ command, args });
44
+ if (command === "where") {
45
+ return { code: 0, stdout: "C:\\Tools\\rtk.exe\r\nC:\\Other\\rtk.exe\r\n", stderr: "" };
46
+ }
47
+ return { code: 3, stdout: "rtk git status", stderr: "" };
48
+ },
49
+ } as unknown as ExtensionAPI;
50
+
51
+ const result = await resolveRtkRewrite(pi, "git status", { platform: "win32" });
52
+
53
+ assert.equal(result.changed, true);
54
+ assert.equal(result.executableResolution?.resolvedPath, "C:\\Tools\\rtk.exe");
55
+ assert.deepEqual(calls.map((call) => call.command), ["where", "C:\\Tools\\rtk.exe"]);
56
+ });
57
+
58
+ await runTest("rtk rewrite preserves behavior when executable path resolution fails", async () => {
59
+ const calls: Array<{ command: string; args: string[] }> = [];
60
+ const pi = {
61
+ exec: async (command: string, args: string[]) => {
62
+ calls.push({ command, args });
63
+ if (command === "which") {
64
+ return { code: 1, stdout: "", stderr: "not found" };
65
+ }
66
+ return { code: 3, stdout: "rtk git status", stderr: "" };
67
+ },
68
+ } as unknown as ExtensionAPI;
69
+
70
+ const result = await resolveRtkRewrite(pi, "git status", { platform: "linux" });
71
+
72
+ assert.equal(result.changed, true);
73
+ assert.equal(result.executableResolution?.command, "rtk");
74
+ assert.ok(result.executableResolution?.warning?.includes("which failed"));
75
+ assert.deepEqual(calls.map((call) => call.command), ["which", "rtk"]);
76
+ });
77
+
78
+ await runTest("empty command unchanged", async () => {
79
+ const config = cloneDefaultConfig();
80
+ const decision = await computeRewriteDecision("", config, createMockPi({ code: 1 }));
81
+ assert.equal(decision.changed, false);
82
+ assert.equal(decision.reason, "empty");
83
+ });
84
+
85
+ await runTest("already rtk unchanged", async () => {
86
+ const config = cloneDefaultConfig();
87
+ const decision = await computeRewriteDecision("rtk status", config, createMockPi({ code: 1 }));
88
+ assert.equal(decision.changed, false);
89
+ assert.equal(decision.reason, "already_rtk");
90
+ });
91
+
92
+ await runTest("rtk unsupported heredoc result leaves command unchanged", async () => {
93
+ const config = cloneDefaultConfig();
94
+ const decision = await computeRewriteDecision("cat <<EOF", config, createMockPi({ code: 1 }));
95
+ assert.equal(decision.changed, false);
96
+ assert.equal(decision.reason, "no_match");
97
+ });
98
+
99
+ await runTest("quoted heredoc marker is delegated to RTK rewrite", async () => {
100
+ const config = cloneDefaultConfig();
101
+ const command = 'echo "<<not heredoc" && git status';
102
+ const decision = await computeRewriteDecision(
103
+ command,
104
+ config,
105
+ createMockPi({ code: 3, stdout: 'echo "<<not heredoc" && rtk git status' }),
106
+ );
107
+ assert.equal(decision.changed, true);
108
+ assert.equal(decision.rewrittenCommand, 'echo "<<not heredoc" && rtk git status');
109
+ assert.equal(decision.reason, "ok");
110
+ });
111
+
112
+ await runTest("legacy category toggles do not pre-filter RTK rewrite source of truth", async () => {
113
+ const config = { ...cloneDefaultConfig(), rewriteGitGithub: false };
114
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
115
+ assert.equal(decision.changed, true);
116
+ assert.equal(decision.rewrittenCommand, "rtk git status");
117
+ assert.equal(decision.reason, "ok");
118
+ });
119
+
120
+ await runTest("rtk exit 0 rewrites", async () => {
121
+ const config = cloneDefaultConfig();
122
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 0, stdout: "rtk git status" }));
123
+ assert.equal(decision.changed, true);
124
+ assert.equal(decision.rewrittenCommand, "rtk git status");
125
+ assert.equal(decision.reason, "ok");
126
+ });
127
+
128
+ await runTest("rtk exit 3 rewrites", async () => {
129
+ const config = cloneDefaultConfig();
130
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
131
+ assert.equal(decision.changed, true);
132
+ assert.equal(decision.rewrittenCommand, "rtk git status");
133
+ assert.equal(decision.reason, "ok");
134
+ });
135
+
136
+ await runTest("exit 1 leaves unchanged", async () => {
137
+ const config = cloneDefaultConfig();
138
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 1 }));
139
+ assert.equal(decision.changed, false);
140
+ assert.equal(decision.reason, "no_match");
141
+ });
142
+
143
+ await runTest("exit 2 leaves unchanged and surfaces RTK detail", async () => {
144
+ const config = cloneDefaultConfig();
145
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 2, stderr: "denied" }));
146
+ assert.equal(decision.changed, false);
147
+ assert.equal(decision.reason, "no_match");
148
+ assert.equal(decision.warning, "denied");
149
+ });
150
+
151
+ await runTest("unknown category passes through to RTK", async () => {
152
+ const config = cloneDefaultConfig();
153
+ const pi = createMockPi({ code: 0, stdout: "rtk custom" });
154
+ const decision = await computeRewriteDecision("custom-cmd", config, pi);
155
+ assert.equal(decision.changed, true);
156
+ assert.equal(decision.rewrittenCommand, "rtk custom");
157
+ assert.equal(decision.reason, "ok");
158
+ });
159
+
160
+ await runTest("exec error/timeout leaves unchanged and surfaces error detail", async () => {
161
+ const config = cloneDefaultConfig();
162
+ const pi = {
163
+ exec: async () => {
164
+ throw new Error("timeout");
165
+ },
166
+ } as unknown as ExtensionAPI;
167
+ const decision = await computeRewriteDecision("git status", config, pi);
168
+ assert.equal(decision.changed, false);
169
+ assert.equal(decision.reason, "no_match");
170
+ assert.equal(decision.warning, "timeout");
171
+ });
172
+
173
+ await runTest("compound commands forwarded to RTK", async () => {
174
+ const config = cloneDefaultConfig();
175
+ let capturedArgs: string[] = [];
176
+ const pi = {
177
+ exec: async (_cmd: string, args: string[]) => {
178
+ capturedArgs = args;
179
+ return { code: 0, stdout: "rtk result" };
180
+ },
181
+ } as unknown as ExtensionAPI;
182
+ const decision = await computeRewriteDecision("git status && cargo test", config, pi);
183
+ assert.equal(decision.changed, true);
184
+ assert.deepEqual(capturedArgs, ["rewrite", "git status && cargo test"]);
185
+ });
186
+
187
+ console.log("All command-rewriter tests passed.");
@@ -1,4 +1,4 @@
1
- import { resolveRtkRewrite } from "./rtk-rewrite-provider.js";
1
+ import { resolveRtkRewrite, type RtkRewriteProviderOptions } from "./rtk-rewrite-provider.js";
2
2
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
3
  import type { RtkIntegrationConfig } from "./types.js";
4
4
 
@@ -7,12 +7,14 @@ export interface RewriteDecision {
7
7
  originalCommand: string;
8
8
  rewrittenCommand: string;
9
9
  reason: "ok" | "empty" | "already_rtk" | "no_match";
10
+ warning?: string;
10
11
  }
11
12
 
12
13
  export async function computeRewriteDecision(
13
14
  command: string,
14
15
  _config: RtkIntegrationConfig,
15
16
  pi: ExtensionAPI,
17
+ rewriteOptions: RtkRewriteProviderOptions = {},
16
18
  ): Promise<RewriteDecision> {
17
19
  if (!command || !command.trim()) {
18
20
  return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "empty" };
@@ -23,7 +25,7 @@ export async function computeRewriteDecision(
23
25
  return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "already_rtk" };
24
26
  }
25
27
 
26
- const result = await resolveRtkRewrite(pi, command);
28
+ const result = await resolveRtkRewrite(pi, command, rewriteOptions);
27
29
 
28
30
  if (result.changed && result.rewrittenCommand) {
29
31
  return {
@@ -39,5 +41,6 @@ export async function computeRewriteDecision(
39
41
  originalCommand: command,
40
42
  rewrittenCommand: command,
41
43
  reason: "no_match",
44
+ warning: result.error,
42
45
  };
43
46
  }
@@ -8,26 +8,40 @@ mock.module("@mariozechner/pi-coding-agent", () => ({
8
8
  getSettingsListTheme: () => ({}),
9
9
  }));
10
10
 
11
+ const settingsListInputs: string[] = [];
12
+ const settingsListUpdates: Array<{ id: string; value: string }> = [];
13
+
11
14
  mock.module("@mariozechner/pi-tui", () => ({
12
- Box: class {},
15
+ Box: class {
16
+ addChild(): void {}
17
+ },
13
18
  Container: class {
14
19
  addChild(): void {}
15
20
  render(): string[] {
16
- return [];
21
+ return ["settings-content"];
17
22
  }
18
23
  invalidate(): void {}
19
24
  },
20
25
  SettingsList: class {
21
- handleInput(): void {}
22
- updateValue(): void {}
26
+ handleInput(data: string): void {
27
+ settingsListInputs.push(data);
28
+ }
29
+ updateValue(id: string, value: string): void {
30
+ settingsListUpdates.push({ id, value });
31
+ }
23
32
  },
24
33
  Spacer: class {},
25
34
  Text: class {},
26
- truncateToWidth: (text: string) => text,
35
+ truncateToWidth: (text: string, width: number) => text.slice(0, width),
27
36
  visibleWidth: (text: string) => text.length,
28
37
  }));
29
38
 
39
+ function stripAnsi(text: string): string {
40
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
41
+ }
42
+
30
43
  const { registerRtkIntegrationCommand } = await import("./config-modal.ts");
44
+ const { ZellijModal, ZellijSettingsModal } = await import("./zellij-modal.ts");
31
45
  const { getRtkArgumentCompletions } = await import("./command-completions.ts");
32
46
 
33
47
  type Notification = { message: string; level: "info" | "warning" | "error" };
@@ -62,6 +76,60 @@ function lastNotification(notifications: Notification[]): Notification {
62
76
  return notifications[notifications.length - 1] as Notification;
63
77
  }
64
78
 
79
+ function createThemeStub(): { fg: (_name: string, text: string) => string; bold: (text: string) => string } {
80
+ return {
81
+ fg: (_name: string, text: string) => text,
82
+ bold: (text: string) => text,
83
+ };
84
+ }
85
+
86
+ runTest("zellij settings modal renders overlay frame and delegates non-enter input", () => {
87
+ settingsListInputs.length = 0;
88
+ settingsListUpdates.length = 0;
89
+ const settingsModal = new ZellijSettingsModal(
90
+ {
91
+ title: "RTK Integration Settings",
92
+ settings: [
93
+ {
94
+ id: "enabled",
95
+ label: "Enabled",
96
+ description: "Enable integration",
97
+ currentValue: "on",
98
+ values: ["on", "off"],
99
+ },
100
+ ],
101
+ onChange: () => {},
102
+ onClose: () => {},
103
+ helpText: "Esc: close",
104
+ },
105
+ createThemeStub() as never,
106
+ );
107
+ const modal = new ZellijModal(settingsModal, {
108
+ titleBar: {
109
+ left: { text: "RTK Integration Settings", maxWidth: 30, color: "accent" },
110
+ right: { text: "pi-rtk-optimizer", maxWidth: 20, color: "dim" },
111
+ },
112
+ helpUndertitle: { text: "Esc: close", color: "dim" },
113
+ overlay: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
114
+ });
115
+
116
+ const rendered = modal.renderModal(86);
117
+ settingsModal.handleInput("\r");
118
+ settingsModal.handleInput("j");
119
+ settingsModal.updateValue("enabled", "off");
120
+
121
+ assert.equal(rendered.visibleWidth, 86);
122
+ assert.equal(rendered.contentWidth, 82);
123
+ assert.ok(stripAnsi(rendered.lines[0] ?? "").includes("RTK Integration Settings"));
124
+ assert.ok(stripAnsi(rendered.lines[rendered.lines.length - 1] ?? "").includes("Esc: close"));
125
+ assert.deepEqual(modal.getOverlayOptions(), {
126
+ overlay: true,
127
+ overlayOptions: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
128
+ });
129
+ assert.deepEqual(settingsListInputs, ["j"]);
130
+ assert.deepEqual(settingsListUpdates, [{ id: "enabled", value: "off" }]);
131
+ });
132
+
65
133
  runTest("command completions return top-level and filtered RTK subcommands", () => {
66
134
  const topLevel = getRtkArgumentCompletions("");
67
135
  assert.ok(Array.isArray(topLevel));
@@ -77,12 +145,7 @@ runTest("command completions return top-level and filtered RTK subcommands", ()
77
145
  assert.equal(getRtkArgumentCompletions("zzz"), null);
78
146
  });
79
147
 
80
- async function runAsyncTest(name: string, testFn: () => Promise<void>): Promise<void> {
81
- await testFn();
82
- console.log(`[PASS] ${name}`);
83
- }
84
-
85
- await runAsyncTest("config modal command handlers route RTK subcommands to controller actions", async () => {
148
+ await runTest("config modal command handlers route RTK subcommands to controller actions", async () => {
86
149
  const config = cloneDefaultConfig();
87
150
  const controllerState = {
88
151
  config,
@@ -101,7 +164,7 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
101
164
  getRuntimeStatus: () => ({ rtkAvailable: false, lastError: "not found" }),
102
165
  refreshRuntimeStatus: async () => {
103
166
  controllerState.refreshed += 1;
104
- return { rtkAvailable: false, lastError: "not found" };
167
+ return { rtkAvailable: true, rtkExecutablePath: "C:/Tools/rtk.exe" };
105
168
  },
106
169
  getMetricsSummary: () => "metrics summary",
107
170
  clearMetrics: () => {
@@ -110,15 +173,16 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
110
173
  };
111
174
 
112
175
  let registeredName = "";
113
- let definition: {
176
+ type RegisteredCommandDefinition = {
114
177
  description: string;
115
178
  getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
116
179
  handler: (args: string, ctx: CommandContextStub) => Promise<void>;
117
- } | null = null;
180
+ };
181
+ let definition: RegisteredCommandDefinition | undefined;
118
182
 
119
183
  registerRtkIntegrationCommand(
120
184
  {
121
- registerCommand(name: string, nextDefinition: typeof definition) {
185
+ registerCommand(name: string, nextDefinition: RegisteredCommandDefinition) {
122
186
  registeredName = name;
123
187
  definition = nextDefinition;
124
188
  },
@@ -127,44 +191,46 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
127
191
  );
128
192
 
129
193
  assert.equal(registeredName, "rtk");
130
- assert.ok(definition !== null);
131
- assert.ok((definition?.description ?? "").includes("Configure RTK rewrite"));
132
- assert.ok(typeof definition?.getArgumentCompletions === "function");
194
+ if (!definition) {
195
+ throw new Error("Expected /rtk command definition to be registered");
196
+ }
197
+ assert.ok(definition.description.includes("Configure RTK rewrite"));
198
+ assert.ok(typeof definition.getArgumentCompletions === "function");
133
199
 
134
200
  const infoCtx = createNotifyContext(true);
135
- await definition?.handler("help", infoCtx.ctx);
201
+ await definition.handler("help", infoCtx.ctx);
136
202
  assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
137
203
 
138
- await definition?.handler("show", infoCtx.ctx);
204
+ await definition.handler("show", infoCtx.ctx);
139
205
  assert.ok(lastNotification(infoCtx.notifications).message.includes("mode=rewrite"));
140
206
  assert.ok(lastNotification(infoCtx.notifications).message.includes("rewriteSource=rtk"));
141
207
  assert.equal(lastNotification(infoCtx.notifications).message.includes("categories="), false);
142
208
 
143
- await definition?.handler("path", infoCtx.ctx);
209
+ await definition.handler("path", infoCtx.ctx);
144
210
  assert.equal(lastNotification(infoCtx.notifications).message, "rtk config: C:/tmp/pi-rtk-optimizer/config.json");
145
211
 
146
- await definition?.handler("verify", infoCtx.ctx);
212
+ await definition.handler("verify", infoCtx.ctx);
147
213
  assert.equal(controllerState.refreshed, 1);
148
- assert.equal(lastNotification(infoCtx.notifications).level, "warning");
149
- assert.ok(lastNotification(infoCtx.notifications).message.includes("not available: not found"));
214
+ assert.equal(lastNotification(infoCtx.notifications).level, "info");
215
+ assert.ok(lastNotification(infoCtx.notifications).message.includes("available at C:/Tools/rtk.exe"));
150
216
 
151
- await definition?.handler("stats", infoCtx.ctx);
217
+ await definition.handler("stats", infoCtx.ctx);
152
218
  assert.equal(lastNotification(infoCtx.notifications).message, "metrics summary");
153
219
 
154
- await definition?.handler("clear-stats", infoCtx.ctx);
220
+ await definition.handler("clear-stats", infoCtx.ctx);
155
221
  assert.equal(controllerState.cleared, 1);
156
222
  assert.equal(lastNotification(infoCtx.notifications).message, "RTK metrics cleared.");
157
223
 
158
- await definition?.handler("reset", infoCtx.ctx);
224
+ await definition.handler("reset", infoCtx.ctx);
159
225
  assert.equal(controllerState.lastSavedMode, "rewrite");
160
226
  assert.equal(lastNotification(infoCtx.notifications).message, "RTK integration settings reset to defaults.");
161
227
 
162
- await definition?.handler("unknown", infoCtx.ctx);
228
+ await definition.handler("unknown", infoCtx.ctx);
163
229
  assert.equal(lastNotification(infoCtx.notifications).level, "warning");
164
230
  assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
165
231
 
166
232
  const headlessCtx = createNotifyContext(false);
167
- await definition?.handler("", headlessCtx.ctx);
233
+ await definition.handler("", headlessCtx.ctx);
168
234
  assert.equal(lastNotification(headlessCtx.notifications).message, "/rtk requires interactive TUI mode.");
169
235
  });
170
236
 
@@ -53,12 +53,21 @@ function parseIntegerInRange(value: string, min: number, max: number): number |
53
53
  return parsed;
54
54
  }
55
55
 
56
- function summarizeConfig(config: RtkIntegrationConfig, runtimeStatus: RuntimeStatus): string {
56
+ function summarizeRuntimeStatus(runtimeStatus: RuntimeStatus): string {
57
57
  const runtime = runtimeStatus.rtkAvailable
58
58
  ? "rtk=available"
59
59
  : `rtk=missing${runtimeStatus.lastError ? ` (${runtimeStatus.lastError})` : ""}`;
60
+ const executable = runtimeStatus.rtkExecutablePath
61
+ ? `, rtkPath=${runtimeStatus.rtkExecutablePath}`
62
+ : runtimeStatus.rtkExecutableResolutionWarning
63
+ ? `, rtkPath=unresolved (${runtimeStatus.rtkExecutableResolutionWarning})`
64
+ : "";
65
+
66
+ return `${runtime}${executable}`;
67
+ }
60
68
 
61
- return `enabled=${config.enabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${runtime}`;
69
+ function summarizeConfig(config: RtkIntegrationConfig, runtimeStatus: RuntimeStatus): string {
70
+ return `enabled=${config.enabled}, mode=${config.mode}, rewriteSource=rtk, rewriteNotice=${config.showRewriteNotifications}, compaction=${config.outputCompaction.enabled}, readCompaction=${config.outputCompaction.readCompaction.enabled}, sourceFilterEnabled=${config.outputCompaction.sourceCodeFilteringEnabled}, preserveSkillReads=${config.outputCompaction.preserveExactSkillReads}, sourceFilter=${config.outputCompaction.sourceCodeFiltering}, ${summarizeRuntimeStatus(runtimeStatus)}`;
62
71
  }
63
72
 
64
73
  function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
@@ -105,6 +114,13 @@ function buildSettingItems(config: RtkIntegrationConfig): SettingItem[] {
105
114
  currentValue: toOnOff(config.outputCompaction.stripAnsi),
106
115
  values: ON_OFF,
107
116
  },
117
+ {
118
+ id: "outputReadCompactionEnabled",
119
+ label: "Read compaction enabled",
120
+ description: "If off, read tool output stays exact; build/test/git/grep compaction can still run",
121
+ currentValue: toOnOff(config.outputCompaction.readCompaction.enabled),
122
+ values: ON_OFF,
123
+ },
108
124
  {
109
125
  id: "outputTruncateEnabled",
110
126
  label: "Hard truncation enabled",
@@ -219,6 +235,14 @@ function applySetting(config: RtkIntegrationConfig, id: string, value: string):
219
235
  ...config,
220
236
  outputCompaction: { ...config.outputCompaction, stripAnsi: value === "on" },
221
237
  };
238
+ case "outputReadCompactionEnabled":
239
+ return {
240
+ ...config,
241
+ outputCompaction: {
242
+ ...config.outputCompaction,
243
+ readCompaction: { enabled: value === "on" },
244
+ },
245
+ };
222
246
  case "outputTruncateEnabled":
223
247
  return {
224
248
  ...config,
@@ -353,6 +377,7 @@ function syncSettingValues(settingsList: SettingValueSyncTarget, config: RtkInte
353
377
  settingsList.updateValue("guardWhenRtkMissing", toOnOff(config.guardWhenRtkMissing));
354
378
  settingsList.updateValue("outputCompactionEnabled", toOnOff(config.outputCompaction.enabled));
355
379
  settingsList.updateValue("outputStripAnsi", toOnOff(config.outputCompaction.stripAnsi));
380
+ settingsList.updateValue("outputReadCompactionEnabled", toOnOff(config.outputCompaction.readCompaction.enabled));
356
381
  settingsList.updateValue("outputTruncateEnabled", toOnOff(config.outputCompaction.truncate.enabled));
357
382
  settingsList.updateValue("outputTruncateMaxChars", String(config.outputCompaction.truncate.maxChars));
358
383
  settingsList.updateValue("outputSourceFilteringEnabled", toOnOff(config.outputCompaction.sourceCodeFilteringEnabled));
@@ -458,7 +483,8 @@ async function handleArgs(
458
483
  if (normalized === "verify") {
459
484
  const runtimeStatus = await controller.refreshRuntimeStatus();
460
485
  if (runtimeStatus.rtkAvailable) {
461
- ctx.ui.notify("RTK binary is available.", "info");
486
+ const pathDetail = runtimeStatus.rtkExecutablePath ? ` at ${runtimeStatus.rtkExecutablePath}` : "";
487
+ ctx.ui.notify(`RTK binary is available${pathDetail}.`, "info");
462
488
  } else {
463
489
  ctx.ui.notify(
464
490
  `RTK binary is not available${runtimeStatus.lastError ? `: ${runtimeStatus.lastError}` : ""}.`,