@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.6

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 (207) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
  3. package/dist/cli.js +2989 -2978
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +23 -1
  7. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  8. package/dist/types/internal-urls/parse.d.ts +12 -0
  9. package/dist/types/internal-urls/router.d.ts +6 -0
  10. package/dist/types/internal-urls/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +1 -0
  12. package/dist/types/lsp/types.d.ts +2 -0
  13. package/dist/types/mcp/manager.d.ts +5 -0
  14. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  15. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  16. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  17. package/dist/types/sdk.d.ts +3 -1
  18. package/dist/types/session/agent-session-types.d.ts +3 -1
  19. package/dist/types/session/agent-session.d.ts +2 -0
  20. package/dist/types/session/model-controls.d.ts +3 -0
  21. package/dist/types/session/session-advisors.d.ts +7 -1
  22. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  23. package/dist/types/session/turn-recovery.d.ts +4 -2
  24. package/dist/types/task/index.d.ts +1 -1
  25. package/dist/types/task/types.d.ts +3 -11
  26. package/dist/types/thinking.d.ts +21 -2
  27. package/dist/types/utils/title-generator.d.ts +17 -16
  28. package/package.json +12 -12
  29. package/src/capability/index.ts +43 -12
  30. package/src/capability/mcp.ts +21 -0
  31. package/src/capability/types.ts +20 -1
  32. package/src/cli/read-cli.ts +44 -2
  33. package/src/config/settings-schema.ts +27 -1
  34. package/src/eval/py/runner.py +16 -2
  35. package/src/extensibility/extensions/runner.ts +91 -5
  36. package/src/extensibility/extensions/types.ts +0 -1
  37. package/src/extensibility/hooks/types.ts +0 -1
  38. package/src/internal-urls/mcp-protocol.ts +17 -3
  39. package/src/internal-urls/parse.ts +31 -0
  40. package/src/internal-urls/router.ts +24 -4
  41. package/src/internal-urls/types.ts +6 -0
  42. package/src/live/transport.ts +2 -2
  43. package/src/lsp/client.ts +2 -2
  44. package/src/lsp/config.ts +4 -0
  45. package/src/lsp/types.ts +2 -0
  46. package/src/mcp/config.ts +26 -14
  47. package/src/mcp/manager.ts +26 -9
  48. package/src/mcp/tool-bridge.ts +52 -1
  49. package/src/memories/index.ts +25 -6
  50. package/src/modes/components/status-line/segments.ts +3 -1
  51. package/src/modes/controllers/command-controller.ts +10 -10
  52. package/src/modes/controllers/extension-ui-controller.ts +3 -3
  53. package/src/modes/rpc/rpc-mode.ts +60 -47
  54. package/src/prompts/steering/user-interjection.md +2 -5
  55. package/src/prompts/tools/task.md +4 -2
  56. package/src/sdk.ts +17 -8
  57. package/src/session/agent-session-types.ts +3 -0
  58. package/src/session/agent-session.ts +21 -4
  59. package/src/session/model-controls.ts +43 -7
  60. package/src/session/session-advisors.ts +30 -14
  61. package/src/session/session-tools.ts +4 -2
  62. package/src/session/tool-choice-queue.ts +19 -4
  63. package/src/session/turn-recovery.ts +21 -2
  64. package/src/task/executor.ts +11 -3
  65. package/src/task/index.ts +43 -32
  66. package/src/task/types.ts +12 -17
  67. package/src/thinking.ts +68 -5
  68. package/src/tools/read.ts +2 -2
  69. package/src/utils/title-generator.ts +88 -34
  70. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  71. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  72. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  73. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  74. package/dist/types/cli/update-cli.test.d.ts +0 -1
  75. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  76. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  77. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  78. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  79. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  80. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  81. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  82. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  83. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  84. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  85. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  86. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  87. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  88. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  89. package/dist/types/hindsight/client.test.d.ts +0 -1
  90. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  91. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  92. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  93. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  94. package/dist/types/launch/protocol.test.d.ts +0 -1
  95. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  96. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  97. package/dist/types/live/protocol.test.d.ts +0 -1
  98. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  99. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  100. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  101. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  102. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  103. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  104. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  105. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  106. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  107. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  108. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  109. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  110. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  111. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  112. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  113. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  114. package/dist/types/modes/print-mode.test.d.ts +0 -1
  115. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  116. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  117. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  118. package/dist/types/modes/warp-events.test.d.ts +0 -1
  119. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  120. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  121. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  122. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  123. package/dist/types/session/blob-store.test.d.ts +0 -1
  124. package/dist/types/session/messages.test.d.ts +0 -1
  125. package/dist/types/session/session-context.test.d.ts +0 -1
  126. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  127. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  128. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  129. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  130. package/dist/types/system-prompt.test.d.ts +0 -1
  131. package/dist/types/task/render.test.d.ts +0 -1
  132. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  133. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  134. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  135. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  136. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  137. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  138. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  139. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  140. package/src/advisor/__tests__/config.test.ts +0 -349
  141. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  142. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  143. package/src/cli/update-cli.test.ts +0 -28
  144. package/src/config/__tests__/model-registry.test.ts +0 -182
  145. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  146. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  147. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  148. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  149. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  150. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  151. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  152. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  153. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  154. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  155. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  156. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  157. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  158. package/src/hindsight/client.test.ts +0 -75
  159. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  160. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  161. package/src/launch/broker-list-order.test.ts +0 -89
  162. package/src/launch/broker-output-snapshot.test.ts +0 -126
  163. package/src/launch/protocol.test.ts +0 -59
  164. package/src/launch/spawn-options.test.ts +0 -31
  165. package/src/launch/terminal-output.test.ts +0 -107
  166. package/src/live/protocol.test.ts +0 -140
  167. package/src/mcp/config-writer.test.ts +0 -43
  168. package/src/mcp/smithery-auth.test.ts +0 -29
  169. package/src/mcp/smithery-registry.test.ts +0 -51
  170. package/src/mcp/transports/stdio.test.ts +0 -427
  171. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  172. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  173. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  174. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  175. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  176. package/src/modes/components/custom-editor.test.ts +0 -510
  177. package/src/modes/components/login-dialog.test.ts +0 -56
  178. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  179. package/src/modes/components/status-line/component.test.ts +0 -84
  180. package/src/modes/components/tool-execution.test.ts +0 -162
  181. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  182. package/src/modes/noninteractive-dispose.test.ts +0 -73
  183. package/src/modes/print-mode.test.ts +0 -71
  184. package/src/modes/session-teardown.test.ts +0 -219
  185. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  186. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  187. package/src/modes/warp-events.test.ts +0 -794
  188. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  189. package/src/plan-mode/model-transition.test.ts +0 -60
  190. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  191. package/src/session/agent-session-error-log.test.ts +0 -59
  192. package/src/session/blob-store.test.ts +0 -56
  193. package/src/session/messages.test.ts +0 -282
  194. package/src/session/session-context.test.ts +0 -384
  195. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  196. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  197. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  198. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  199. package/src/system-prompt.test.ts +0 -236
  200. package/src/task/render.test.ts +0 -290
  201. package/src/task/spawn-policy.test.ts +0 -62
  202. package/src/tools/__tests__/eval-description.test.ts +0 -18
  203. package/src/tools/__tests__/glob.test.ts +0 -37
  204. package/src/tools/__tests__/json-tree.test.ts +0 -35
  205. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  206. package/src/tools/hub/launch-compat.test.ts +0 -40
  207. package/src/vibe/__tests__/token-rate.test.ts +0 -96
@@ -1,252 +0,0 @@
1
- import { afterEach, beforeAll, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
- import * as fs from "node:fs";
3
- import * as fsp from "node:fs/promises";
4
- import * as os from "node:os";
5
- import * as path from "node:path";
6
- import { visibleWidth } from "@oh-my-pi/pi-tui";
7
- import { Settings } from "../../../config/settings";
8
- import { getThemeByName, setThemeInstance, type Theme } from "../../theme/theme";
9
- import { MoveOverlay, type MoveOverlayResult, resolveExistingDirectory, resolveMovePath } from "../move-overlay";
10
-
11
- // Strip SGR colors so assertions see visible text only.
12
- const stripAnsi = (text: string): string => text.replace(/\x1b\[[0-9;]*m/g, "");
13
- const strip = (lines: readonly string[]): string => lines.map(stripAnsi).join("\n");
14
-
15
- describe("resolveMovePath", () => {
16
- it("expands ~ to homedir", () => {
17
- expect(resolveMovePath("~", "/anywhere")).toBe(os.homedir());
18
- });
19
- it("expands ~/sub to homedir/sub", () => {
20
- expect(resolveMovePath("~/foo", "/anywhere")).toBe(path.join(os.homedir(), "foo"));
21
- });
22
- it("resolves relative paths against cwd", () => {
23
- expect(resolveMovePath("foo/bar", "/parent")).toBe(path.resolve("/parent", "foo/bar"));
24
- });
25
- it("passes absolute paths through (normalized)", () => {
26
- expect(resolveMovePath("/abs/path", "/anywhere")).toBe(path.normalize("/abs/path"));
27
- });
28
- });
29
-
30
- describe("resolveExistingDirectory", () => {
31
- let tmp: string;
32
-
33
- beforeEach(async () => {
34
- tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-move-resolve-"));
35
- });
36
- afterEach(async () => {
37
- await fsp.rm(tmp, { recursive: true, force: true });
38
- });
39
-
40
- it("returns the resolved path for an existing directory", () => {
41
- const sub = path.join(tmp, "sub");
42
- fs.mkdirSync(sub);
43
- expect(resolveExistingDirectory(sub, "/anywhere")).toBe(path.resolve(sub));
44
- });
45
- it("returns null for a non-existent path", () => {
46
- expect(resolveExistingDirectory(path.join(tmp, "nope"), "/anywhere")).toBeNull();
47
- });
48
- it("returns null for a file (not a directory)", () => {
49
- const file = path.join(tmp, "file.txt");
50
- fs.writeFileSync(file, "x");
51
- expect(resolveExistingDirectory(file, "/anywhere")).toBeNull();
52
- });
53
- });
54
-
55
- describe("MoveOverlay", () => {
56
- let tmp: string;
57
- let cwd: string;
58
- let uiTheme: Theme;
59
-
60
- beforeAll(async () => {
61
- await Settings.init({ inMemory: true });
62
- const loaded = await getThemeByName("dark");
63
- if (!loaded) throw new Error("theme unavailable");
64
- uiTheme = loaded;
65
- setThemeInstance(uiTheme);
66
- });
67
-
68
- beforeEach(async () => {
69
- tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-move-overlay-"));
70
- cwd = tmp;
71
- fs.mkdirSync(path.join(tmp, "alpha"));
72
- fs.mkdirSync(path.join(tmp, "beta"));
73
- fs.mkdirSync(path.join(tmp, ".hidden"));
74
- fs.writeFileSync(path.join(tmp, "file.txt"), "x");
75
- });
76
- afterEach(async () => {
77
- await fsp.rm(tmp, { recursive: true, force: true });
78
- });
79
-
80
- it("renders a box with a title and input prompt", () => {
81
- const overlay = new MoveOverlay(cwd, () => {});
82
- const text = strip(overlay.render(80));
83
- expect(text).toContain("Move to directory");
84
- expect(text).toContain("Path:");
85
- });
86
-
87
- it("renders every frame row at the assigned overlay width", () => {
88
- const overlay = new MoveOverlay(cwd, () => {});
89
- const lines = overlay.render(72);
90
- const plainLines = lines.map(stripAnsi);
91
-
92
- expect(lines.map(line => visibleWidth(line))).toEqual(Array(lines.length).fill(72));
93
- expect(plainLines[0]!.endsWith(uiTheme.boxRound.topRight)).toBe(true);
94
- expect(plainLines.at(-1)!.endsWith(uiTheme.boxRound.bottomRight)).toBe(true);
95
- for (const line of plainLines.slice(1, -1)) {
96
- expect(line.endsWith(uiTheme.boxRound.vertical)).toBe(true);
97
- }
98
- });
99
-
100
- it("lists child directories (excluding hidden and files) on empty input", () => {
101
- const overlay = new MoveOverlay(cwd, () => {});
102
- const text = strip(overlay.render(80));
103
- expect(text).toContain("alpha/");
104
- expect(text).toContain("beta/");
105
- expect(text).not.toContain(".hidden/");
106
- expect(text).not.toContain("file.txt");
107
- });
108
-
109
- it("filters results as the user types", () => {
110
- const overlay = new MoveOverlay(cwd, () => {});
111
- overlay.handleInput("a");
112
- overlay.handleInput("l");
113
- const text = strip(overlay.render(80));
114
- expect(text).toContain("alpha/");
115
- expect(text).not.toContain("beta/");
116
- });
117
-
118
- it("shows dot directories after a dot prefix is typed", () => {
119
- const overlay = new MoveOverlay(cwd, () => {});
120
- overlay.handleInput(".");
121
- const text = strip(overlay.render(80));
122
- expect(text).toContain(".hidden/");
123
- });
124
-
125
- it("accepts bracketed paste and multi-byte input while filtering controls", () => {
126
- let result: MoveOverlayResult | undefined;
127
- const overlay = new MoveOverlay(cwd, r => {
128
- result = r;
129
- });
130
- overlay.handleInput("\x1b[200~new\nø\x1b[201~");
131
- overlay.handleInput("\r");
132
- expect(result).toBeDefined();
133
- expect(result!.directory).toBe("newø");
134
- });
135
-
136
- it("calls done with undefined on Escape", () => {
137
- let result: MoveOverlayResult | undefined = "sentinel" as unknown as MoveOverlayResult;
138
- const overlay = new MoveOverlay(cwd, r => {
139
- result = r;
140
- });
141
- overlay.handleInput("\x1b");
142
- expect(result).toBeUndefined();
143
- });
144
-
145
- it("calls done with the highlighted directory on Enter", () => {
146
- let result: MoveOverlayResult | undefined;
147
- const overlay = new MoveOverlay(cwd, r => {
148
- result = r;
149
- });
150
- // First result should be "alpha/" (sorted alphabetically).
151
- overlay.handleInput("\r");
152
- expect(result).toBeDefined();
153
- expect(result!.directory).toBe(path.join(cwd, "alpha"));
154
- });
155
-
156
- it("calls done with the typed path on Enter when no results match", () => {
157
- let result: MoveOverlayResult | undefined;
158
- const overlay = new MoveOverlay(cwd, r => {
159
- result = r;
160
- });
161
- // Type a path that won't match any directory in cwd.
162
- overlay.handleInput("z");
163
- overlay.handleInput("z");
164
- overlay.handleInput("\r");
165
- expect(result).toBeDefined();
166
- expect(result!.directory).toBe("zz");
167
- });
168
-
169
- it("Tab accepts the highlighted suggestion into the input", () => {
170
- let result: MoveOverlayResult | undefined;
171
- const overlay = new MoveOverlay(cwd, r => {
172
- result = r;
173
- });
174
- overlay.handleInput("\t");
175
- // After tab, the input should be the full path of the first result.
176
- // Press Enter to confirm — the result should be the alpha directory.
177
- overlay.handleInput("\r");
178
- expect(result).toBeDefined();
179
- expect(result!.directory).toBe(path.join(cwd, "alpha"));
180
- });
181
-
182
- it("does not stat every directory entry per keystroke", async () => {
183
- // Regression: `readDirCached` used to cache names and re-`statSync` every
184
- // entry per keystroke. Populate with mostly files so the old code path
185
- // would have stat'd all of them; the fix classifies via `Dirent` and only
186
- // falls back to `statSync` on symlink entries.
187
- const bulk = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-move-overlay-bulk-"));
188
- try {
189
- for (let i = 0; i < 60; i++) {
190
- fs.writeFileSync(path.join(bulk, `f_${String(i).padStart(3, "0")}.txt`), "x");
191
- }
192
- for (let i = 0; i < 10; i++) fs.mkdirSync(path.join(bulk, `sub_${i}`));
193
-
194
- const statSpy = spyOn(fs, "statSync");
195
- try {
196
- const overlay = new MoveOverlay(bulk, () => {});
197
- statSpy.mockClear();
198
- overlay.handleInput("s");
199
- overlay.handleInput("u");
200
- overlay.handleInput("b");
201
- // Each keystroke may `statSync` at most once — the
202
- // `resolveExistingDirectory` probe on the typed prefix. Entries
203
- // are classified via `Dirent`, not one stat apiece.
204
- expect(statSpy.mock.calls.length).toBeLessThanOrEqual(3);
205
- } finally {
206
- statSpy.mockRestore();
207
- }
208
- } finally {
209
- await fsp.rm(bulk, { recursive: true, force: true });
210
- }
211
- });
212
-
213
- it("classifies unknown-type Dirents via statSync fallback", async () => {
214
- // Regression: filesystems that report UV_DIRENT_UNKNOWN return Dirents
215
- // whose isDirectory()/isFile()/isSymbolicLink() all report false. Those
216
- // entries must fall back to statSync so /move still lists real
217
- // directories on NFS/FUSE/older SMB.
218
- const unknownFs = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-move-overlay-unknown-"));
219
- try {
220
- const realDir = path.join(unknownFs, "real-dir");
221
- const realFile = path.join(unknownFs, "real-file.txt");
222
- fs.mkdirSync(realDir);
223
- fs.writeFileSync(realFile, "x");
224
-
225
- const fakeDirent = (name: string): fs.Dirent =>
226
- ({
227
- name,
228
- isDirectory: () => false,
229
- isFile: () => false,
230
- isSymbolicLink: () => false,
231
- isBlockDevice: () => false,
232
- isCharacterDevice: () => false,
233
- isFIFO: () => false,
234
- isSocket: () => false,
235
- }) as fs.Dirent;
236
- const readdirSpy = spyOn(fs, "readdirSync").mockReturnValue([
237
- fakeDirent("real-dir"),
238
- fakeDirent("real-file.txt"),
239
- ] as never);
240
- try {
241
- const overlay = new MoveOverlay(unknownFs, () => {});
242
- const text = strip(overlay.render(80));
243
- expect(text).toContain("real-dir/");
244
- expect(text).not.toContain("real-file.txt");
245
- } finally {
246
- readdirSpy.mockRestore();
247
- }
248
- } finally {
249
- await fsp.rm(unknownFs, { recursive: true, force: true });
250
- }
251
- });
252
- });
@@ -1,143 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, it } from "bun:test";
2
- import { agentPauseGate } from "@oh-my-pi/pi-agent-core";
3
- import type { Component } from "@oh-my-pi/pi-tui";
4
- import { Settings } from "../../../config/settings";
5
- import { getThemeByName, setThemeInstance } from "../../theme/theme";
6
- import { PauseScreenComponent, type PauseScreenHost, renderPauseScreen, runPauseScreen } from "../pause-screen";
7
-
8
- // Strip SGR colors so assertions see visible text only.
9
- const stripAnsi = (text: string): string => text.replace(/\x1b\[[0-9;]*m/g, "");
10
-
11
- interface FakeHost {
12
- host: PauseScreenHost;
13
- shown: Component[];
14
- statuses: string[];
15
- hiddenCount(): number;
16
- }
17
-
18
- function makeHost(rows = 24): FakeHost {
19
- const shown: Component[] = [];
20
- const statuses: string[] = [];
21
- let hidden = 0;
22
- const host: PauseScreenHost = {
23
- ui: {
24
- showOverlay(component) {
25
- shown.push(component);
26
- return {
27
- hide: () => {
28
- hidden++;
29
- },
30
- setHidden() {},
31
- isHidden: () => false,
32
- };
33
- },
34
- setFocus() {},
35
- requestRender() {},
36
- terminal: { rows },
37
- },
38
- showStatus(message) {
39
- statuses.push(message);
40
- },
41
- };
42
- return { host, shown, statuses, hiddenCount: () => hidden };
43
- }
44
-
45
- describe("pause screen", () => {
46
- beforeAll(async () => {
47
- await Settings.init({ inMemory: true });
48
- const loaded = await getThemeByName("dark");
49
- if (!loaded) throw new Error("theme unavailable");
50
- setThemeInstance(loaded);
51
- });
52
-
53
- afterEach(() => {
54
- // The gate is process-global: never leak an engaged pause into other files.
55
- agentPauseGate.resume();
56
- });
57
-
58
- describe("renderPauseScreen", () => {
59
- it("paints exactly the requested rows with title, explainer, clock, and hint", () => {
60
- const lines = renderPauseScreen(80, 24, 65_000);
61
- expect(lines.length).toBe(24);
62
- const text = lines.map(stripAnsi).join("\n");
63
- expect(text).toContain("P A U S E D");
64
- expect(text).toContain("Main agent, subagents, and advisor");
65
- expect(text).toContain("paused for 1:05");
66
- expect(text).toContain("esc · enter · space — resume");
67
- expect(text).toContain("█".repeat(5));
68
- });
69
-
70
- it("drops to the compact card on small terminals", () => {
71
- const lines = renderPauseScreen(40, 10, 3_000);
72
- expect(lines.length).toBe(10);
73
- const text = lines.map(stripAnsi).join("\n");
74
- expect(text).toContain("▌▌ P A U S E D");
75
- expect(text).toContain("paused for 0:03");
76
- expect(text).toContain("esc to resume");
77
- expect(text).not.toContain("█".repeat(5)); // no room for the big glyph
78
- });
79
-
80
- it("rolls the clock into hours past 60 minutes", () => {
81
- const text = renderPauseScreen(80, 24, 3_725_000).map(stripAnsi).join("\n");
82
- expect(text).toContain("paused for 1:02:05");
83
- });
84
-
85
- it("displays the session name when provided in full mode", () => {
86
- const lines = renderPauseScreen(80, 24, 65_000, "My Awesome Session");
87
- const text = lines.map(stripAnsi).join("\n");
88
- expect(text).toContain("My Awesome Session");
89
- expect(text).toContain("P A U S E D");
90
- });
91
-
92
- it("displays the session name when provided in compact mode", () => {
93
- const lines = renderPauseScreen(40, 10, 3_000, "Compact Session Title");
94
- const text = lines.map(stripAnsi).join("\n");
95
- expect(text).toContain("Compact Session Title");
96
- expect(text).toContain("▌▌ P A U S E D");
97
- });
98
- });
99
-
100
- describe("runPauseScreen", () => {
101
- it("engages the gate for the screen's lifetime and releases it on escape", async () => {
102
- const { host, shown, statuses, hiddenCount } = makeHost();
103
- expect(agentPauseGate.paused).toBe(false);
104
-
105
- const run = runPauseScreen(host);
106
- await Bun.sleep(1);
107
- expect(agentPauseGate.paused).toBe(true);
108
- expect(shown.length).toBe(1);
109
-
110
- const component = shown[0];
111
- expect(component).toBeInstanceOf(PauseScreenComponent);
112
- if (component instanceof PauseScreenComponent) {
113
- component.handleInput("\x1b"); // escape → resume
114
- }
115
- await run;
116
-
117
- expect(agentPauseGate.paused).toBe(false);
118
- expect(hiddenCount()).toBe(1);
119
- expect(statuses.some(message => message.includes("Resumed after"))).toBe(true);
120
- });
121
-
122
- it("treats ctrl+c as resume, never as abort-and-stay-paused", async () => {
123
- const { host, shown } = makeHost();
124
- const run = runPauseScreen(host);
125
- await Bun.sleep(1);
126
-
127
- const component = shown[0];
128
- if (component instanceof PauseScreenComponent) {
129
- component.handleInput("\x03"); // ctrl+c
130
- }
131
- await run;
132
- expect(agentPauseGate.paused).toBe(false);
133
- });
134
-
135
- it("is a no-op when the gate is already engaged elsewhere", async () => {
136
- agentPauseGate.pause();
137
- const { host, shown } = makeHost();
138
- await runPauseScreen(host); // must resolve immediately, not park
139
- expect(shown.length).toBe(0);
140
- expect(agentPauseGate.paused).toBe(true); // foreign pause not stolen
141
- });
142
- });
143
- });
@@ -1,94 +0,0 @@
1
- import { beforeAll, describe, expect, it } from "bun:test";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
- import { Settings } from "../../../config/settings";
5
- import type { CustomMessage, SkillPromptDetails } from "../../../session/messages";
6
- import { getThemeByName, setThemeInstance, type Theme } from "../../theme/theme";
7
- import { SkillMessageComponent } from "../skill-message";
8
-
9
- // Drop SGR colors and OSC 8 hyperlink wrappers so assertions see the visible text only.
10
- const strip = (lines: readonly string[]): string =>
11
- lines
12
- .join("\n")
13
- .replace(/\x1b\]8;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "")
14
- .replace(/\x1b\[[0-9;]*m/g, "");
15
-
16
- function makeMessage(
17
- details: SkillPromptDetails,
18
- content = "Use the atomic-commit workflow.",
19
- ): CustomMessage<SkillPromptDetails> {
20
- return { role: "custom", customType: "skill-prompt", content, display: true, details, timestamp: Date.now() };
21
- }
22
-
23
- describe("SkillMessageComponent", () => {
24
- let uiTheme: Theme;
25
-
26
- beforeAll(async () => {
27
- await Settings.init({ inMemory: true });
28
- const loaded = await getThemeByName("dark");
29
- if (!loaded) throw new Error("theme unavailable");
30
- uiTheme = loaded;
31
- setThemeInstance(uiTheme);
32
- });
33
-
34
- const skillPath = path.join(os.homedir(), ".agent/skills/atomic-commit/SKILL.md");
35
-
36
- it("renders a compact, outlined card instead of the archaic key:value dump", () => {
37
- const component = new SkillMessageComponent(
38
- makeMessage({ name: "atomic-commit", path: skillPath, lineCount: 88 }),
39
- );
40
- const text = strip(component.render(80));
41
-
42
- // New look: an icon-tagged "skill" header with the name and a single meta line.
43
- expect(text).toContain("skill");
44
- expect(text).toContain("atomic-commit");
45
- expect(text).toContain("skill atomic-commit");
46
- expect(text).not.toContain("skill atomic-commit");
47
- expect(text).toContain("88 lines");
48
-
49
- // The card is drawn with an outline.
50
- expect(text).toContain(uiTheme.boxRound.topLeft);
51
- expect(text).toContain(uiTheme.boxRound.bottomRight);
52
-
53
- // Path is home-shortened and never leaks the absolute home dir.
54
- expect(text).toContain("~/.agent/skills/atomic-commit/SKILL.md");
55
- expect(text).not.toContain(os.homedir());
56
-
57
- // The old archaic framing is gone.
58
- expect(text).not.toContain("[skill]");
59
- expect(text).not.toContain("Skill:");
60
- expect(text).not.toContain("Path:");
61
- expect(text).not.toContain("Prompt:");
62
- });
63
-
64
- it("flattens multi-line args onto the single-line header", () => {
65
- const component = new SkillMessageComponent(
66
- makeMessage({ name: "atomic-commit", path: skillPath, lineCount: 88, args: "stage all\nthen split" }),
67
- );
68
- const text = strip(component.render(80));
69
- // Whitespace (including the newline) collapsed to single spaces so the header can't break.
70
- expect(text).toContain("stage all then split");
71
- expect(text).not.toContain("stage all\nthen split");
72
- });
73
-
74
- it("uses a singular unit for a one-line prompt", () => {
75
- const component = new SkillMessageComponent(makeMessage({ name: "tiny", path: skillPath, lineCount: 1 }));
76
- const text = strip(component.render(80));
77
- expect(text).toContain("1 line");
78
- expect(text).not.toContain("1 lines");
79
- });
80
-
81
- it("reveals the prompt body under a calm subheader only when expanded", () => {
82
- const details: SkillPromptDetails = { name: "atomic-commit", path: skillPath, lineCount: 88 };
83
- const body = "Step one: stage hunks.";
84
-
85
- const collapsed = new SkillMessageComponent(makeMessage(details, body));
86
- expect(strip(collapsed.render(80))).not.toContain(body);
87
-
88
- const expanded = new SkillMessageComponent(makeMessage(details, body));
89
- expanded.setExpanded(true);
90
- const text = strip(expanded.render(80));
91
- expect(text).toContain("prompt");
92
- expect(text).toContain(body);
93
- });
94
- });
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { ProcessTerminal, TUI } from "@oh-my-pi/pi-tui";
3
- import { getEditorTheme, initTheme } from "../theme/theme";
4
- import { CustomEditor } from "./custom-editor";
5
-
6
- /**
7
- * Regression for issue #4766: plugins written against upstream pi subclass
8
- * `CustomEditor`/`Editor` and forward `super(tui, theme, keybindings)`. omp's
9
- * `setEditorComponent` factory contract advertises exactly that arg order, so
10
- * the base constructor must resolve the real theme by shape (not position) or
11
- * every render throws `undefined is not an object (evaluating
12
- * 'this.#theme.symbols.boxRound')`.
13
- */
14
- describe("CustomEditor upstream-pi constructor compatibility (#4766)", () => {
15
- it("renders when constructed as (tui, theme, keybindings)", async () => {
16
- await initTheme();
17
- const tui = new TUI(new ProcessTerminal());
18
- const editor = new CustomEditor(tui, getEditorTheme(), {});
19
- editor.setText("run this workflow");
20
- expect(() => editor.render(80)).not.toThrow();
21
- // The rounded border glyphs from the resolved theme must reach the frame.
22
- const frame = editor.render(80).join("\n");
23
- expect(frame).toContain(getEditorTheme().symbols.boxRound.horizontal);
24
- // The leading TUI is captured so plugin overrides calling
25
- // `this.tui.requestRender()` keep working.
26
- expect(editor.tui).toBe(tui);
27
- });
28
-
29
- it("still accepts omp's own (theme) constructor", async () => {
30
- await initTheme();
31
- const editor = new CustomEditor(getEditorTheme());
32
- editor.setText("hello");
33
- expect(() => editor.render(80)).not.toThrow();
34
- expect(editor.tui).toBeUndefined();
35
- });
36
- });