@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,250 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
2
- import { Container, setKeybindings } from "@oh-my-pi/pi-tui";
3
- import { KeybindingsManager } from "../../config/keybindings";
4
- import type { ExtensionAskDialogQuestion, ExtensionUIContext } from "../../extensibility/extensions";
5
- import { AskDialogComponent } from "../components/ask-dialog";
6
- import { CustomEditor } from "../components/custom-editor";
7
- import { getEditorTheme, getThemeByName, setThemeInstance } from "../theme/theme";
8
- import type { InteractiveModeContext } from "../types";
9
- import { ExtensionUiController } from "./extension-ui-controller";
10
-
11
- afterEach(() => {
12
- setKeybindings(KeybindingsManager.inMemory());
13
- });
14
-
15
- beforeAll(async () => {
16
- const dark = await getThemeByName("dark");
17
- if (!dark) throw new Error("Failed to load dark theme");
18
- setThemeInstance(dark);
19
- });
20
-
21
- function makeHarness() {
22
- const editor = new CustomEditor(getEditorTheme());
23
- const editorContainer = new Container();
24
- editorContainer.addChild(editor);
25
- const requestRender = vi.fn();
26
- const setFocus = vi.fn();
27
- const addAutocompleteProvider = vi.fn();
28
- let uiContext: ExtensionUIContext | undefined;
29
- const ctx = {
30
- editor,
31
- ui: {
32
- requestRender,
33
- setFocus,
34
- terminal: { rows: 40 },
35
- },
36
- editorContainer,
37
- session: {
38
- extensionRunner: undefined,
39
- setUsageFallbackConfirmer: vi.fn(),
40
- },
41
- setToolUIContext(context: ExtensionUIContext, hasUI: boolean): void {
42
- expect(hasUI).toBe(true);
43
- uiContext = context;
44
- },
45
- addAutocompleteProvider,
46
- } as unknown as InteractiveModeContext;
47
-
48
- const controller = new ExtensionUiController(ctx);
49
-
50
- return {
51
- editor,
52
- requestRender,
53
- addAutocompleteProvider,
54
- editorContainer,
55
- setFocus,
56
- controller,
57
- async init(): Promise<ExtensionUIContext> {
58
- await controller.initHooksAndCustomTools();
59
- expect(uiContext).toBeDefined();
60
- return uiContext!;
61
- },
62
- };
63
- }
64
-
65
- describe("ExtensionUiController editor UI", () => {
66
- it("requests a render after extension pasteToEditor mutates the prompt", async () => {
67
- const harness = makeHarness();
68
- const ui = await harness.init();
69
-
70
- ui.pasteToEditor("hello");
71
- ui.pasteToEditor(" world");
72
-
73
- expect(harness.editor.getText()).toBe("hello world");
74
- expect(harness.requestRender).toHaveBeenCalledTimes(2);
75
- });
76
-
77
- it("requests a render after extension setEditorText replaces the prompt", async () => {
78
- const harness = makeHarness();
79
- const ui = await harness.init();
80
-
81
- ui.setEditorText("hello");
82
-
83
- expect(harness.editor.getText()).toBe("hello");
84
- expect(harness.requestRender).toHaveBeenCalledTimes(1);
85
- });
86
-
87
- it("keeps a populated prompt visible and routes input to it until the draft is cleared", async () => {
88
- const harness = makeHarness();
89
- harness.editor.setText("finish this wor");
90
- const questions: ExtensionAskDialogQuestion[] = [
91
- { id: "confirm", question: "Continue?", options: [{ label: "Yes" }, { label: "No" }] },
92
- ];
93
-
94
- const pending = harness.controller.showAskDialog(questions);
95
- const ask = harness.editorContainer.children[0];
96
- expect(ask).toBeInstanceOf(AskDialogComponent);
97
- expect(harness.editorContainer.children).toEqual([ask, harness.editor]);
98
-
99
- ask?.handleInput?.("d");
100
- expect(harness.editor.getText()).toBe("finish this word");
101
-
102
- harness.editor.setText("");
103
- ask?.handleInput?.("\n");
104
- expect(await pending).toEqual({
105
- kind: "submit",
106
- results: [
107
- {
108
- id: "confirm",
109
- question: "Continue?",
110
- options: ["Yes", "No"],
111
- multi: false,
112
- selectedOptions: ["Yes"],
113
- customInput: undefined,
114
- note: undefined,
115
- timedOut: undefined,
116
- },
117
- ],
118
- });
119
- expect(harness.editorContainer.children).toEqual([harness.editor]);
120
- });
121
-
122
- it("does not fire editor-slot shortcuts that would orphan the ask dialog (#6738)", () => {
123
- const harness = makeHarness();
124
- harness.editor.setText("draft in progress");
125
- // Simulate an editor-slot shortcut like the Agent Hub binding, whose
126
- // handler clears editorContainer and would strand the pending ask.
127
- let hubOpened = false;
128
- harness.editor.setCustomKeyHandler("ctrl+s", () => {
129
- hubOpened = true;
130
- harness.editorContainer.clear();
131
- });
132
- const questions: ExtensionAskDialogQuestion[] = [
133
- { id: "confirm", question: "Continue?", options: [{ label: "Yes" }, { label: "No" }] },
134
- ];
135
-
136
- harness.controller.showAskDialog(questions);
137
- const ask = harness.editorContainer.children[0];
138
- expect(ask).toBeInstanceOf(AskDialogComponent);
139
-
140
- // Ctrl+S reaches the draft editor while ask is open; the shortcut must be
141
- // swallowed, the draft untouched, and the ask surface preserved.
142
- ask?.handleInput?.("\x13");
143
- expect(hubOpened).toBe(false);
144
- expect(harness.editor.getText()).toBe("draft in progress");
145
- expect(harness.editorContainer.children).toEqual([ask, harness.editor]);
146
- });
147
-
148
- it("exposes the draft editor cursor while it proxies input, and drops it once cleared (#6738)", () => {
149
- const harness = makeHarness();
150
- harness.editor.setText("finish this wor");
151
- const questions: ExtensionAskDialogQuestion[] = [
152
- { id: "confirm", question: "Continue?", options: [{ label: "Yes" }, { label: "No" }] },
153
- ];
154
-
155
- harness.controller.showAskDialog(questions);
156
- const ask = harness.editorContainer.children[0];
157
- expect(ask).toBeInstanceOf(AskDialogComponent);
158
-
159
- // The ask dialog holds TUI focus, but rendering it must mirror focus onto
160
- // the draft editor so its insertion cursor is visible.
161
- ask?.render?.(80);
162
- expect(harness.editor.focused).toBe(true);
163
-
164
- // Once the draft clears, the ask controls take over and the editor cursor
165
- // must not linger.
166
- harness.editor.setText("");
167
- ask?.render?.(80);
168
- expect(harness.editor.focused).toBe(false);
169
- });
170
-
171
- it("lets the clear action empty the draft and lift the ask guard (#6738)", () => {
172
- const harness = makeHarness();
173
- // Route Ctrl+C to the guard: keep app.clear on Ctrl+C but move the ask
174
- // cancel key off it, so Ctrl+C reaches draft editing instead of cancelling.
175
- setKeybindings(KeybindingsManager.inMemory({ "tui.select.cancel": "ctrl+g" }));
176
- harness.editor.setActionKeys("app.clear", ["ctrl+c"]);
177
- let cleared = 0;
178
- // Mirror interactive wiring: app.clear (Ctrl+C) clears the draft.
179
- harness.editor.onClear = () => {
180
- cleared++;
181
- harness.editor.setText("");
182
- };
183
- harness.editor.setText("half typed prompt");
184
- const questions: ExtensionAskDialogQuestion[] = [
185
- { id: "confirm", question: "Continue?", options: [{ label: "Yes" }, { label: "No" }] },
186
- ];
187
-
188
- harness.controller.showAskDialog(questions);
189
- const ask = harness.editorContainer.children[0];
190
- expect(ask).toBeInstanceOf(AskDialogComponent);
191
-
192
- // Ctrl+C is reserved by the base editor and never clears; the guard must
193
- // dispatch the configured clear action so the "finish or clear" hint works.
194
- ask?.handleInput?.("\x03");
195
- expect(cleared).toBe(1);
196
- expect(harness.editor.getText()).toBe("");
197
-
198
- // With the draft gone the guard releases: the next key reaches the ask
199
- // controls and submits the highlighted option.
200
- ask?.handleInput?.("\n");
201
- expect(harness.editorContainer.children).toEqual([harness.editor]);
202
- });
203
-
204
- it("remounts the draft editor when the ask surface is restored after a nested prompt (#6738)", async () => {
205
- const harness = makeHarness();
206
- harness.editor.setText("half typed prompt");
207
- const questions: ExtensionAskDialogQuestion[] = [
208
- { id: "confirm", question: "Continue?", options: [{ label: "Yes" }, { label: "No" }] },
209
- ];
210
-
211
- harness.controller.showAskDialog(questions);
212
- const ask = harness.editorContainer.children[0];
213
- expect(ask).toBeInstanceOf(AskDialogComponent);
214
- expect(harness.editorContainer.children).toEqual([ask, harness.editor]);
215
-
216
- // Draft submitted: the guard lifts and ask controls take input; open the
217
- // note prompt, which swaps the container to the nested editor.
218
- harness.editor.setText("");
219
- ask?.handleInput?.("n");
220
- const promptEditor = harness.editorContainer.children[0];
221
- expect(promptEditor).not.toBe(ask);
222
-
223
- // A failed async submission restores the draft while the nested prompt is
224
- // open, re-blocking the guard.
225
- harness.editor.setText("half typed prompt");
226
-
227
- // Cancelling the nested prompt restores the ask surface; the draft editor
228
- // must be remounted so routed input lands on a visible surface.
229
- promptEditor?.handleInput?.("\x1b");
230
- expect(harness.editorContainer.children).toEqual([ask, harness.editor]);
231
- // The dialog's prompt-active latch clears when the awaited onPrompt
232
- // promise settles; yield a microtask before routing the next key.
233
- await Promise.resolve();
234
- ask?.handleInput?.("!");
235
- expect(harness.editor.getText()).toBe("half typed prompt!");
236
- });
237
-
238
- it("bridges addAutocompleteProvider factories to the interactive mode context (#4919)", async () => {
239
- const harness = makeHarness();
240
- const ui = await harness.init();
241
-
242
- expect(typeof ui.addAutocompleteProvider).toBe("function");
243
-
244
- const factory = (current: unknown) => current as never;
245
- ui.addAutocompleteProvider(factory);
246
-
247
- expect(harness.addAutocompleteProvider).toHaveBeenCalledTimes(1);
248
- expect(harness.addAutocompleteProvider).toHaveBeenCalledWith(factory);
249
- });
250
- });
@@ -1,73 +0,0 @@
1
- /**
2
- * Contract: the print-mode assistant-error/aborted exit path MUST run the
3
- * awaited `session.dispose()` (which contains the bounded browser reaper
4
- * `releaseTabsForOwner`) before terminating the process. It previously called
5
- * `process.exit(1)` ahead of the `dispose()` at the end of `runPrintMode`, so
6
- * an OMP-owned Chromium survived the exit (issue #5643).
7
- */
8
- import { describe, expect, it, spyOn } from "bun:test";
9
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
10
- import type { AgentSession } from "../session/agent-session";
11
- import * as telemetryExport from "../telemetry-export";
12
- import { runPrintMode } from "./print-mode";
13
-
14
- /** Stand-in for `process.exit`: it terminates, so nothing after it should run. */
15
- class ProcessExit extends Error {
16
- constructor(readonly code: number) {
17
- super(`process.exit(${code})`);
18
- }
19
- }
20
-
21
- describe("print-mode error exit disposes the session before exit", () => {
22
- it("disposes on the assistant-error path before process.exit(1)", async () => {
23
- const order: string[] = [];
24
- const errorMsg: AssistantMessage = {
25
- role: "assistant",
26
- content: [],
27
- api: "openai-responses",
28
- provider: "openai",
29
- model: "gpt-test",
30
- usage: {} as AssistantMessage["usage"],
31
- stopReason: "error",
32
- errorMessage: "boom",
33
- timestamp: 1,
34
- };
35
- const session = {
36
- extensionRunner: undefined,
37
- subscribe: () => {},
38
- settings: { get: () => false },
39
- sessionManager: { buildSessionContext: () => ({ messages: [] }), getEntries: () => [] },
40
- state: { messages: [errorMsg] },
41
- getLastAssistantMessage: () => errorMsg,
42
- prepareForHeadlessAdvisorDrain: () => {},
43
- waitForAdvisorCatchup: async () => {
44
- order.push("catchup");
45
- return true;
46
- },
47
- dispose: async () => {
48
- order.push("dispose");
49
- },
50
- } as unknown as AgentSession;
51
-
52
- const flushSpy = spyOn(telemetryExport, "flushTelemetryExport").mockImplementation(async () => {
53
- order.push("flush");
54
- });
55
- const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
56
- order.push("exit");
57
- throw new ProcessExit(code);
58
- }) as never);
59
- const stderrSpy = spyOn(process.stderr, "write").mockImplementation((() => true) as never);
60
-
61
- try {
62
- await runPrintMode(session, { mode: "text" });
63
- } catch (err) {
64
- if (!(err instanceof ProcessExit)) throw err;
65
- } finally {
66
- exitSpy.mockRestore();
67
- stderrSpy.mockRestore();
68
- flushSpy.mockRestore();
69
- }
70
-
71
- expect(order).toEqual(["catchup", "flush", "dispose", "exit"]);
72
- });
73
- });
@@ -1,71 +0,0 @@
1
- /**
2
- * Contract: `--mode json` output stays linear in conversation size. Streaming
3
- * `message_update` events must not re-serialize the in-progress message, and
4
- * printed messages must not carry provider-opaque replay payloads (encrypted
5
- * reasoning history), which previously produced multi-GB transcripts.
6
- */
7
- import { describe, expect, it } from "bun:test";
8
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
9
- import type { AgentSessionEvent } from "../session/agent-session";
10
- import { printableEvent } from "./print-mode";
11
-
12
- const assistant: AssistantMessage = {
13
- role: "assistant",
14
- content: [{ type: "text", text: "hello" }],
15
- api: "openai-responses",
16
- provider: "openai",
17
- model: "gpt-test",
18
- usage: {} as AssistantMessage["usage"],
19
- stopReason: "stop",
20
- timestamp: 1,
21
- providerPayload: { type: "openaiResponsesHistory", provider: "openai", dt: true, items: [{ big: "blob" }] },
22
- };
23
-
24
- describe("printableEvent", () => {
25
- it("emits only the incremental delta for message_update", () => {
26
- const event: AgentSessionEvent = {
27
- type: "message_update",
28
- message: assistant,
29
- assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "hel", partial: assistant },
30
- };
31
- const printed = JSON.parse(JSON.stringify(printableEvent(event)));
32
- expect(printed).toEqual({
33
- type: "message_update",
34
- assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "hel" },
35
- });
36
- });
37
-
38
- it("drops the done-variant message snapshot from message_update", () => {
39
- const event: AgentSessionEvent = {
40
- type: "message_update",
41
- message: assistant,
42
- assistantMessageEvent: { type: "done", reason: "stop", message: assistant },
43
- };
44
- const printed = JSON.parse(JSON.stringify(printableEvent(event)));
45
- expect(printed).toEqual({ type: "message_update", assistantMessageEvent: { type: "done", reason: "stop" } });
46
- });
47
-
48
- it("strips providerPayload from message_end but keeps the message content", () => {
49
- const event: AgentSessionEvent = { type: "message_end", message: assistant };
50
- const printed = JSON.parse(JSON.stringify(printableEvent(event))) as {
51
- message: Record<string, unknown>;
52
- };
53
- expect(printed.message.providerPayload).toBeUndefined();
54
- expect(printed.message.content).toEqual([{ type: "text", text: "hello" }]);
55
- expect(printed.message.model).toBe("gpt-test");
56
- });
57
-
58
- it("strips providerPayload from every message in agent_end", () => {
59
- const event: AgentSessionEvent = { type: "agent_end", messages: [assistant, assistant] };
60
- const printed = JSON.parse(JSON.stringify(printableEvent(event))) as {
61
- messages: Array<Record<string, unknown>>;
62
- };
63
- expect(printed.messages).toHaveLength(2);
64
- for (const message of printed.messages) expect(message.providerPayload).toBeUndefined();
65
- });
66
-
67
- it("passes unrelated events through untouched", () => {
68
- const event: AgentSessionEvent = { type: "notice", level: "info", message: "hi" };
69
- expect(printableEvent(event)).toBe(event);
70
- });
71
- });
@@ -1,219 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { postmortem } from "@oh-my-pi/pi-utils";
3
- import { createSessionTeardown } from "./session-teardown";
4
-
5
- /**
6
- * Signal-safe session teardown contract (issue #4080). The callback body
7
- * registered on `postmortem` for `SIGINT`/`SIGTERM`/`SIGHUP`/`uncaughtException`
8
- * must persist the in-progress editor draft and emit the extension
9
- * `session_shutdown` event (via `session.dispose()`) — the same steps the TUI
10
- * Ctrl+C keypress path performs. Both paths funnel through
11
- * `createSessionTeardown`, so exercising it directly proves the acceptance
12
- * criteria hold regardless of the trigger.
13
- */
14
- describe("createSessionTeardown", () => {
15
- it("persists the draft, then disposes the session, in that order", async () => {
16
- const order: string[] = [];
17
- const saved: string[] = [];
18
-
19
- const teardown = createSessionTeardown({
20
- getDraftText: () => "unsent draft",
21
- beginDispose: () => {
22
- order.push("beginDispose");
23
- },
24
- saveDraft: async text => {
25
- order.push("saveDraft");
26
- saved.push(text);
27
- },
28
- disposeSession: async () => {
29
- order.push("disposeSession");
30
- },
31
- });
32
-
33
- await teardown();
34
-
35
- expect(order).toEqual(["beginDispose", "saveDraft", "disposeSession"]);
36
- expect(saved).toEqual(["unsent draft"]);
37
- });
38
-
39
- it("marks the session disposing before awaiting draft persistence", async () => {
40
- const order: string[] = [];
41
- const release = Promise.withResolvers<void>();
42
-
43
- const teardown = createSessionTeardown({
44
- getDraftText: () => {
45
- order.push("snapshot");
46
- return "draft";
47
- },
48
- beginDispose: () => {
49
- order.push("beginDispose");
50
- },
51
- saveDraft: async () => {
52
- order.push("saveDraft:start");
53
- await release.promise;
54
- order.push("saveDraft:done");
55
- },
56
- disposeSession: async () => {
57
- order.push("disposeSession");
58
- },
59
- });
60
-
61
- const running = teardown();
62
- expect(order).toEqual(["snapshot", "beginDispose", "saveDraft:start"]);
63
- release.resolve();
64
- await running;
65
-
66
- expect(order).toEqual(["snapshot", "beginDispose", "saveDraft:start", "saveDraft:done", "disposeSession"]);
67
- });
68
-
69
- it("still disposes when saveDraft rejects — never leaves session_shutdown unemitted", async () => {
70
- let disposed = false;
71
-
72
- const teardown = createSessionTeardown({
73
- getDraftText: () => "draft",
74
- beginDispose: () => {},
75
- saveDraft: async () => {
76
- throw new Error("disk full");
77
- },
78
- disposeSession: async () => {
79
- disposed = true;
80
- },
81
- });
82
-
83
- await teardown();
84
-
85
- expect(disposed).toBe(true);
86
- });
87
-
88
- it("passes the empty snapshot through so a stale sidecar is cleared on clean exit", async () => {
89
- const seen: string[] = [];
90
- const teardown = createSessionTeardown({
91
- getDraftText: () => "",
92
- beginDispose: () => {},
93
- saveDraft: async text => {
94
- seen.push(text);
95
- },
96
- disposeSession: async () => {},
97
- });
98
-
99
- await teardown();
100
-
101
- expect(seen).toEqual([""]);
102
- });
103
-
104
- it("memoizes: concurrent and repeat calls run the teardown exactly once", async () => {
105
- let getDraftCalls = 0;
106
- let saveDraftCalls = 0;
107
- let disposeCalls = 0;
108
- const release = Promise.withResolvers<void>();
109
-
110
- const teardown = createSessionTeardown({
111
- getDraftText: () => {
112
- getDraftCalls++;
113
- return `draft-${getDraftCalls}`;
114
- },
115
- beginDispose: () => {},
116
- saveDraft: async () => {
117
- saveDraftCalls++;
118
- },
119
- disposeSession: async () => {
120
- disposeCalls++;
121
- await release.promise;
122
- },
123
- });
124
-
125
- // Kick off two concurrent invocations while the first is still awaiting
126
- // disposeSession — this is exactly what happens if a SIGTERM arrives
127
- // mid-`InteractiveMode.shutdown()`.
128
- const first = teardown();
129
- const second = teardown();
130
- release.resolve();
131
- await Promise.all([first, second]);
132
-
133
- // A third call after settlement must still be a no-op.
134
- await teardown();
135
-
136
- expect(getDraftCalls).toBe(1);
137
- expect(saveDraftCalls).toBe(1);
138
- expect(disposeCalls).toBe(1);
139
- });
140
-
141
- it("snapshots the draft text at first call — later editor mutations do not leak in", async () => {
142
- let editorText = "before";
143
- let captured: string | undefined;
144
-
145
- const teardown = createSessionTeardown({
146
- getDraftText: () => editorText,
147
- beginDispose: () => {},
148
- saveDraft: async text => {
149
- captured = text;
150
- },
151
- disposeSession: async () => {},
152
- });
153
-
154
- const running = teardown();
155
- editorText = "after"; // A late edit must not overwrite the persisted draft.
156
- await running;
157
-
158
- expect(captured).toBe("before");
159
- });
160
-
161
- it("forwards the postmortem reason into disposeSession so signal exits record the real trigger", async () => {
162
- const received: Array<postmortem.Reason | undefined> = [];
163
-
164
- const teardown = createSessionTeardown({
165
- getDraftText: () => "",
166
- beginDispose: () => {},
167
- saveDraft: async () => {},
168
- disposeSession: async reason => {
169
- received.push(reason);
170
- },
171
- });
172
-
173
- await teardown(postmortem.Reason.SIGTERM);
174
-
175
- expect(received).toEqual([postmortem.Reason.SIGTERM]);
176
- });
177
-
178
- it("keypress path passes no reason — dispose falls back to a normal exit record", async () => {
179
- const received: Array<postmortem.Reason | undefined> = [];
180
-
181
- const teardown = createSessionTeardown({
182
- getDraftText: () => "",
183
- beginDispose: () => {},
184
- saveDraft: async () => {},
185
- disposeSession: async reason => {
186
- received.push(reason);
187
- },
188
- });
189
-
190
- await teardown();
191
-
192
- expect(received).toEqual([undefined]);
193
- });
194
-
195
- it("first call's reason wins: a later caller with a different reason awaits the same promise", async () => {
196
- const received: Array<postmortem.Reason | undefined> = [];
197
- const release = Promise.withResolvers<void>();
198
-
199
- const teardown = createSessionTeardown({
200
- getDraftText: () => "",
201
- beginDispose: () => {},
202
- saveDraft: async () => {},
203
- disposeSession: async reason => {
204
- received.push(reason);
205
- await release.promise;
206
- },
207
- });
208
-
209
- // SIGTERM lands first; postmortem.quit(0)'s MANUAL pass arrives while the
210
- // teardown is still draining — it must not restart the run or mutate the
211
- // recorded reason.
212
- const first = teardown(postmortem.Reason.SIGTERM);
213
- const second = teardown(postmortem.Reason.MANUAL);
214
- release.resolve();
215
- await Promise.all([first, second]);
216
-
217
- expect(received).toEqual([postmortem.Reason.SIGTERM]);
218
- });
219
- });
@@ -1,53 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, it } from "bun:test";
2
- import { Markdown } from "@oh-my-pi/pi-tui";
3
- import { Settings } from "../../config/settings";
4
- import { buildSystemPrompt } from "../../system-prompt";
5
- import { getMarkdownTheme, getThemeByName, setMarkdownMermaidRendering, setThemeInstance } from "./theme";
6
-
7
- const workspaceTree = {
8
- rootPath: "/tmp/project",
9
- rendered: "",
10
- truncated: false,
11
- totalLines: 0,
12
- agentsMdFiles: [],
13
- };
14
-
15
- function stripAnsi(text: string): string {
16
- return text.replace(/\x1b\[[0-9;]*m/g, "");
17
- }
18
-
19
- beforeAll(async () => {
20
- await Settings.init({ inMemory: true });
21
- const theme = await getThemeByName("dark");
22
- if (!theme) throw new Error("theme unavailable");
23
- setThemeInstance(theme);
24
- });
25
-
26
- afterEach(() => {
27
- setMarkdownMermaidRendering(true);
28
- });
29
-
30
- describe("Mermaid rendering setting", () => {
31
- it("removes the Mermaid prompt note when rendering is disabled", async () => {
32
- const { systemPrompt } = await buildSystemPrompt({
33
- renderMermaid: false,
34
- contextFiles: [],
35
- skills: [],
36
- toolNames: [],
37
- workspaceTree,
38
- });
39
-
40
- expect(systemPrompt.join("\n")).not.toContain("```mermaid");
41
- });
42
-
43
- it("falls back to a highlighted code fence when rendering is disabled", () => {
44
- setMarkdownMermaidRendering(false);
45
-
46
- const markdown = new Markdown("```mermaid\ngraph TD\n A --> B\n```", 0, 0, getMarkdownTheme());
47
- const lines = stripAnsi(markdown.render(80).join("\n"));
48
-
49
- expect(lines).toContain("```mermaid");
50
- expect(lines).toContain("graph TD");
51
- expect(lines).toContain("-->");
52
- });
53
- });