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

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 (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  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 +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. 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
- });