@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,794 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "bun:test";
2
- import * as path from "node:path";
3
- import * as terminalCapabilities from "@oh-my-pi/pi-tui/terminal-capabilities";
4
- import { VERSION } from "@oh-my-pi/pi-utils/dirs";
5
- import type {
6
- AgentEndEvent,
7
- AgentStartEvent,
8
- ExtensionAPI,
9
- ExtensionContext,
10
- InputEvent,
11
- MessageStartEvent,
12
- SessionBranchEvent,
13
- SessionStartEvent,
14
- SessionSwitchEvent,
15
- ToolApprovalRequestedEvent,
16
- } from "../extensibility/extensions/types";
17
- import { SILENT_ABORT_MARKER, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
18
- import { createWarpEventBridgeExtension, createWarpEventEmitter } from "./warp-events";
19
-
20
- const originalTerminalId = terminalCapabilities.TERMINAL.id;
21
- const originalProtocolVersion = process.env.WARP_CLI_AGENT_PROTOCOL_VERSION;
22
- const project = path.basename(process.cwd());
23
- const OSC_PREFIX = "\x1b]777;notify;warp://cli-agent;";
24
-
25
- type RegisteredHandler = (...args: never[]) => void;
26
-
27
- function enableWarpProtocol(terminalId = "warp"): void {
28
- Object.defineProperty(terminalCapabilities.TERMINAL, "id", { value: terminalId, configurable: true });
29
- process.env.WARP_CLI_AGENT_PROTOCOL_VERSION = "1";
30
- }
31
-
32
- function restoreProtocolEnvironment(): void {
33
- Object.defineProperty(terminalCapabilities.TERMINAL, "id", { value: originalTerminalId, configurable: true });
34
- if (originalProtocolVersion === undefined) {
35
- delete process.env.WARP_CLI_AGENT_PROTOCOL_VERSION;
36
- } else {
37
- process.env.WARP_CLI_AGENT_PROTOCOL_VERSION = originalProtocolVersion;
38
- }
39
- }
40
-
41
- function createHandlers(): Map<string, RegisteredHandler> {
42
- const handlers = new Map<string, RegisteredHandler>();
43
- const api = {
44
- on(event: string, handler: RegisteredHandler): void {
45
- handlers.set(event, handler);
46
- },
47
- } as never as ExtensionAPI;
48
- createWarpEventBridgeExtension()(api);
49
- return handlers;
50
- }
51
-
52
- function parseBodies(write: { mock: { calls: unknown[][] } }): Array<Record<string, unknown>> {
53
- return write.mock.calls.map(call => {
54
- const osc = call[0] as string;
55
- return JSON.parse(osc.slice(OSC_PREFIX.length, osc.length - 1)) as Record<string, unknown>;
56
- });
57
- }
58
-
59
- function userMessageStart(text: string, overrides: Partial<MessageStartEvent["message"]> = {}): MessageStartEvent {
60
- return {
61
- type: "message_start",
62
- message: {
63
- role: "user",
64
- content: text,
65
- timestamp: Date.now(),
66
- ...overrides,
67
- } as MessageStartEvent["message"],
68
- };
69
- }
70
-
71
- function skillPromptStart(text: string, attribution: "user" | "agent" = "user"): MessageStartEvent {
72
- return {
73
- type: "message_start",
74
- message: {
75
- role: "custom",
76
- customType: SKILL_PROMPT_MESSAGE_TYPE,
77
- content: text,
78
- display: true,
79
- attribution,
80
- timestamp: Date.now(),
81
- } as MessageStartEvent["message"],
82
- };
83
- }
84
-
85
- function bridgeContext(sessionId = "session-123", cwd = process.cwd()): ExtensionContext {
86
- return {
87
- sessionManager: {
88
- getSessionId: () => sessionId,
89
- getCwd: () => cwd,
90
- },
91
- } as never as ExtensionContext;
92
- }
93
-
94
- afterEach(() => {
95
- vi.restoreAllMocks();
96
- restoreProtocolEnvironment();
97
- });
98
-
99
- describe("Warp CLI-agent events", () => {
100
- it("emits an exact OSC 777 stop event", () => {
101
- enableWarpProtocol();
102
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
103
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
104
- const emitter = createWarpEventEmitter({ sessionId: "session-123" });
105
-
106
- emitter?.emit({ event: "stop" });
107
-
108
- const expectedBody = JSON.stringify({
109
- event: "stop",
110
- v: 1,
111
- agent: "omp",
112
- session_id: "session-123",
113
- cwd: process.cwd(),
114
- project,
115
- plugin_version: VERSION,
116
- });
117
- expect(write).toHaveBeenCalledWith(`${OSC_PREFIX}${expectedBody}\x07`);
118
- });
119
-
120
- it("wraps OSC output when running inside tmux", () => {
121
- enableWarpProtocol();
122
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
123
- const tmux = vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(true);
124
- const wrap = vi.spyOn(terminalCapabilities, "wrapTmuxPassthrough");
125
- const emitter = createWarpEventEmitter({ sessionId: "session-123" });
126
-
127
- emitter?.emit({ event: "stop" });
128
-
129
- expect(tmux).toHaveBeenCalledTimes(1);
130
- expect(wrap).toHaveBeenCalledWith(expect.stringContaining("warp://cli-agent"));
131
- const written = write.mock.calls[0]?.[0] as string;
132
- // Real DCS wrap ends with ST; attention events append outer BEL after it.
133
- expect(written.startsWith("\x1bPtmux;")).toBe(true);
134
- expect(written.endsWith("\x1b\\\x07")).toBe(true);
135
- });
136
-
137
- const attentionEvents = ["stop", "stop_failure", "permission_request", "question_asked"] as const;
138
- const nonAttentionEvents = [
139
- "session_start",
140
- "prompt_submit",
141
- "tool_complete",
142
- "permission_replied",
143
- "custom_event",
144
- ] as const;
145
-
146
- for (const eventName of attentionEvents) {
147
- it(`rings tmux outer BEL for attention event ${eventName}`, () => {
148
- enableWarpProtocol();
149
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
150
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(true);
151
- const emitter = createWarpEventEmitter({ sessionId: "session-123" });
152
-
153
- emitter?.emit({ event: eventName });
154
-
155
- const written = write.mock.calls[0]?.[0] as string;
156
- // Outer BEL after DCS ST; OSC's own \x07 is interior to the passthrough.
157
- expect(written.startsWith("\x1bPtmux;")).toBe(true);
158
- expect(written.endsWith("\x07\x1b\\\x07")).toBe(true);
159
- expect(written.slice(0, -1).endsWith("\x07\x1b\\")).toBe(true);
160
- });
161
- }
162
-
163
- for (const eventName of nonAttentionEvents) {
164
- it(`does not ring tmux outer BEL for non-attention event ${eventName}`, () => {
165
- enableWarpProtocol();
166
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
167
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(true);
168
- const emitter = createWarpEventEmitter({ sessionId: "session-123" });
169
-
170
- emitter?.emit({ event: eventName });
171
-
172
- const written = write.mock.calls[0]?.[0] as string;
173
- // DCS ST only — OSC terminator is inside the wrap, not an outer BEL.
174
- expect(written.startsWith("\x1bPtmux;")).toBe(true);
175
- expect(written.endsWith("\x07\x1b\\")).toBe(true);
176
- expect(written.endsWith("\x07\x1b\\\x07")).toBe(false);
177
- });
178
- }
179
-
180
- it("leaves direct-terminal OSC unchanged without outer BEL after OSC terminator", () => {
181
- enableWarpProtocol();
182
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
183
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
184
- const wrap = vi.spyOn(terminalCapabilities, "wrapTmuxPassthrough");
185
- const emitter = createWarpEventEmitter({ sessionId: "session-123" });
186
-
187
- for (const eventName of [...attentionEvents, ...nonAttentionEvents]) {
188
- write.mockClear();
189
- emitter?.emit({ event: eventName });
190
- const written = write.mock.calls[0]?.[0] as string;
191
- expect(written.startsWith(OSC_PREFIX)).toBe(true);
192
- expect(written.endsWith("\x07")).toBe(true);
193
- // Exactly one trailing BEL (OSC terminator), not an extra attention BEL.
194
- expect(written.endsWith("\x07\x07")).toBe(false);
195
- expect(wrap).not.toHaveBeenCalled();
196
- }
197
- });
198
-
199
- it("creates an emitter from protocol version alone even when terminal id is base", () => {
200
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
201
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
202
- enableWarpProtocol("base");
203
-
204
- const emitter = createWarpEventEmitter({ sessionId: "session-123" });
205
- expect(emitter).toBeDefined();
206
- emitter?.emit({ event: "stop" });
207
-
208
- const body = parseBodies(write)[0];
209
- expect(body).toMatchObject({
210
- event: "stop",
211
- session_id: "session-123",
212
- project,
213
- });
214
- });
215
-
216
- it("does not emit without a negotiated protocol version", () => {
217
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
218
- enableWarpProtocol();
219
-
220
- delete process.env.WARP_CLI_AGENT_PROTOCOL_VERSION;
221
- expect(createWarpEventEmitter({ sessionId: "session-123" })).toBeUndefined();
222
-
223
- process.env.WARP_CLI_AGENT_PROTOCOL_VERSION = "0";
224
- expect(createWarpEventEmitter({ sessionId: "session-123" })).toBeUndefined();
225
-
226
- process.env.WARP_CLI_AGENT_PROTOCOL_VERSION = "not-a-number";
227
- expect(createWarpEventEmitter({ sessionId: "session-123" })).toBeUndefined();
228
- expect(write).not.toHaveBeenCalled();
229
- });
230
-
231
- it("uses session cwd for envelope cwd and project", () => {
232
- enableWarpProtocol();
233
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
234
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
235
- const sessionCwd = "/tmp/session-project-root";
236
- const sessionProject = path.basename(sessionCwd);
237
-
238
- const emitter = createWarpEventEmitter({
239
- sessionId: "session-123",
240
- getCwd: () => sessionCwd,
241
- });
242
- emitter?.emit({ event: "stop" });
243
-
244
- const directBody = parseBodies(write)[0];
245
- expect(directBody).toMatchObject({
246
- event: "stop",
247
- cwd: sessionCwd,
248
- project: sessionProject,
249
- });
250
- expect(directBody?.cwd).not.toBe(process.cwd());
251
-
252
- write.mockClear();
253
- const handlers = createHandlers();
254
- const sessionStart = handlers.get("session_start") as never as (
255
- event: SessionStartEvent,
256
- context: ExtensionContext,
257
- ) => void;
258
- sessionStart({ type: "session_start" }, bridgeContext("session-123", sessionCwd));
259
- const body = parseBodies(write)[0];
260
- expect(body).toMatchObject({
261
- event: "session_start",
262
- cwd: sessionCwd,
263
- project: sessionProject,
264
- });
265
- });
266
-
267
- it("does not resubmit prompts for agent continuations", () => {
268
- enableWarpProtocol();
269
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
270
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
271
- const handlers = createHandlers();
272
- const context = bridgeContext();
273
- const sessionStart = handlers.get("session_start") as never as (
274
- event: SessionStartEvent,
275
- context: ExtensionContext,
276
- ) => void;
277
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
278
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
279
-
280
- sessionStart({ type: "session_start" }, context);
281
- write.mockClear();
282
-
283
- messageStart(userMessageStart("first user prompt"));
284
- // Continuations re-emit agent_start without a user message_start; the bridge must not listen.
285
- expect(handlers.has("agent_start")).toBe(false);
286
- const writesAfterSubmit = write.mock.calls.length;
287
- const agentStart = handlers.get("agent_start") as never as ((event: AgentStartEvent) => void) | undefined;
288
- agentStart?.({ type: "agent_start" });
289
- expect(write.mock.calls.length).toBe(writesAfterSubmit);
290
- agentEnd({
291
- type: "agent_end",
292
- messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] } as never],
293
- });
294
-
295
- const bodies = parseBodies(write);
296
- expect(bodies.filter(body => body.event === "prompt_submit")).toEqual([
297
- expect.objectContaining({ event: "prompt_submit", query: "first user prompt", project }),
298
- ]);
299
- expect(bodies.at(-1)).toMatchObject({
300
- event: "stop",
301
- query: "first user prompt",
302
- response: "done",
303
- project,
304
- });
305
- });
306
-
307
- it("keeps the active query until queued follow-up begins", () => {
308
- enableWarpProtocol();
309
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
310
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
311
- const handlers = createHandlers();
312
- const context = bridgeContext();
313
- const sessionStart = handlers.get("session_start") as never as (
314
- event: SessionStartEvent,
315
- context: ExtensionContext,
316
- ) => void;
317
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
318
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
319
- const input = handlers.get("input") as never as ((event: InputEvent) => void) | undefined;
320
-
321
- sessionStart({ type: "session_start" }, context);
322
- write.mockClear();
323
-
324
- // Early queued input must not overwrite the current response's query.
325
- messageStart(userMessageStart("prompt A"));
326
- input?.({ type: "input", text: "prompt B", source: "interactive" });
327
- agentEnd({
328
- type: "agent_end",
329
- messages: [{ role: "assistant", content: [{ type: "text", text: "answer A" }] } as never],
330
- });
331
-
332
- let bodies = parseBodies(write);
333
- expect(bodies).toEqual([
334
- expect.objectContaining({ event: "prompt_submit", query: "prompt A", project }),
335
- expect.objectContaining({ event: "stop", query: "prompt A", response: "answer A", project }),
336
- ]);
337
-
338
- // After A ends, B's real user message_start owns submit/stop.
339
- write.mockClear();
340
- messageStart(userMessageStart("prompt B"));
341
- agentEnd({
342
- type: "agent_end",
343
- messages: [{ role: "assistant", content: [{ type: "text", text: "answer B" }] } as never],
344
- });
345
- bodies = parseBodies(write);
346
- expect(bodies).toEqual([
347
- expect.objectContaining({ event: "prompt_submit", query: "prompt B", project }),
348
- expect.objectContaining({ event: "stop", query: "prompt B", response: "answer B", project }),
349
- ]);
350
-
351
- // Normal drain: B's message_start arrives before agent_end, so the final pair is B.
352
- write.mockClear();
353
- messageStart(userMessageStart("prompt C"));
354
- messageStart(userMessageStart("prompt D"));
355
- agentEnd({
356
- type: "agent_end",
357
- messages: [{ role: "assistant", content: [{ type: "text", text: "answer D" }] } as never],
358
- });
359
- bodies = parseBodies(write);
360
- expect(bodies).toEqual([
361
- expect.objectContaining({ event: "prompt_submit", query: "prompt C", project }),
362
- expect.objectContaining({ event: "prompt_submit", query: "prompt D", project }),
363
- expect.objectContaining({ event: "stop", query: "prompt D", response: "answer D", project }),
364
- ]);
365
- });
366
-
367
- it("ignores agent-attributed user-role steers", () => {
368
- enableWarpProtocol();
369
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
370
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
371
- const handlers = createHandlers();
372
- const context = bridgeContext();
373
- const sessionStart = handlers.get("session_start") as never as (
374
- event: SessionStartEvent,
375
- context: ExtensionContext,
376
- ) => void;
377
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
378
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
379
-
380
- sessionStart({ type: "session_start" }, context);
381
- write.mockClear();
382
-
383
- messageStart(userMessageStart("prompt A"));
384
- messageStart(userMessageStart("agent steer", { attribution: "agent" }));
385
- agentEnd({
386
- type: "agent_end",
387
- messages: [{ role: "assistant", content: [{ type: "text", text: "answer A" }] } as never],
388
- });
389
-
390
- const bodies = parseBodies(write);
391
- expect(bodies.filter(body => body.event === "prompt_submit")).toEqual([
392
- expect.objectContaining({ event: "prompt_submit", query: "prompt A", project }),
393
- ]);
394
- expect(bodies.at(-1)).toMatchObject({
395
- event: "stop",
396
- query: "prompt A",
397
- response: "answer A",
398
- project,
399
- });
400
- });
401
-
402
- it("treats user-attributed skill prompts as submissions", () => {
403
- enableWarpProtocol();
404
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
405
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
406
- const handlers = createHandlers();
407
- const context = bridgeContext();
408
- const sessionStart = handlers.get("session_start") as never as (
409
- event: SessionStartEvent,
410
- context: ExtensionContext,
411
- ) => void;
412
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
413
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
414
-
415
- sessionStart({ type: "session_start" }, context);
416
- write.mockClear();
417
-
418
- messageStart(skillPromptStart("skill body as query", "user"));
419
- agentEnd({
420
- type: "agent_end",
421
- messages: [{ role: "assistant", content: [{ type: "text", text: "skill answer" }] } as never],
422
- });
423
-
424
- let bodies = parseBodies(write);
425
- expect(bodies).toEqual([
426
- expect.objectContaining({ event: "prompt_submit", query: "skill body as query", project }),
427
- expect.objectContaining({
428
- event: "stop",
429
- query: "skill body as query",
430
- response: "skill answer",
431
- project,
432
- }),
433
- ]);
434
-
435
- // Agent-attributed skill-shaped custom messages are not user submissions.
436
- write.mockClear();
437
- messageStart(userMessageStart("prompt A"));
438
- messageStart(skillPromptStart("auto skill", "agent"));
439
- agentEnd({
440
- type: "agent_end",
441
- messages: [{ role: "assistant", content: [{ type: "text", text: "answer A" }] } as never],
442
- });
443
- bodies = parseBodies(write);
444
- expect(bodies.filter(body => body.event === "prompt_submit")).toEqual([
445
- expect.objectContaining({ event: "prompt_submit", query: "prompt A", project }),
446
- ]);
447
- expect(bodies.at(-1)).toMatchObject({
448
- event: "stop",
449
- query: "prompt A",
450
- response: "answer A",
451
- project,
452
- });
453
- });
454
-
455
- it("falls back to non-silent assistant errorMessage on empty stop responses", () => {
456
- enableWarpProtocol();
457
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
458
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
459
- const handlers = createHandlers();
460
- const context = bridgeContext();
461
- const sessionStart = handlers.get("session_start") as never as (
462
- event: SessionStartEvent,
463
- context: ExtensionContext,
464
- ) => void;
465
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
466
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
467
-
468
- sessionStart({ type: "session_start" }, context);
469
- write.mockClear();
470
-
471
- messageStart(userMessageStart("prompt error"));
472
- agentEnd({
473
- type: "agent_end",
474
- messages: [
475
- {
476
- role: "assistant",
477
- content: [],
478
- stopReason: "error",
479
- errorMessage: "rate limited",
480
- } as never,
481
- ],
482
- });
483
- expect(parseBodies(write).at(-1)).toMatchObject({
484
- event: "stop_failure",
485
- error_type: "error",
486
- query: "prompt error",
487
- response: "rate limited",
488
- });
489
-
490
- // Normal text content is preferred over errorMessage; error stopReason still fails the turn.
491
- write.mockClear();
492
- messageStart(userMessageStart("prompt text"));
493
- agentEnd({
494
- type: "agent_end",
495
- messages: [
496
- {
497
- role: "assistant",
498
- content: [{ type: "text", text: "visible answer" }],
499
- stopReason: "error",
500
- errorMessage: "rate limited",
501
- } as never,
502
- ],
503
- });
504
- expect(parseBodies(write).at(-1)).toMatchObject({
505
- event: "stop_failure",
506
- error_type: "error",
507
- query: "prompt text",
508
- response: "visible answer",
509
- });
510
-
511
- // Silent abort marker must not surface as the stop response or fail the turn.
512
- write.mockClear();
513
- messageStart(userMessageStart("prompt silent"));
514
- agentEnd({
515
- type: "agent_end",
516
- messages: [
517
- {
518
- role: "assistant",
519
- content: [],
520
- stopReason: "aborted",
521
- errorMessage: SILENT_ABORT_MARKER,
522
- } as never,
523
- ],
524
- });
525
- expect(parseBodies(write).at(-1)).toMatchObject({
526
- event: "stop",
527
- query: "prompt silent",
528
- response: "",
529
- });
530
- expect(parseBodies(write).at(-1)).not.toHaveProperty("error_type");
531
-
532
- // User interrupt labels stay suppressed; non-user abort reasons surface as failures.
533
- write.mockClear();
534
- messageStart(userMessageStart("prompt interrupt"));
535
- agentEnd({
536
- type: "agent_end",
537
- messages: [
538
- {
539
- role: "assistant",
540
- content: [],
541
- stopReason: "aborted",
542
- errorMessage: USER_INTERRUPT_LABEL,
543
- } as never,
544
- ],
545
- });
546
- expect(parseBodies(write).at(-1)).toMatchObject({
547
- event: "stop",
548
- query: "prompt interrupt",
549
- response: "",
550
- });
551
-
552
- write.mockClear();
553
- messageStart(userMessageStart("prompt aborted"));
554
- agentEnd({
555
- type: "agent_end",
556
- messages: [
557
- {
558
- role: "assistant",
559
- content: [],
560
- stopReason: "aborted",
561
- errorMessage: "provider cancelled stream",
562
- } as never,
563
- ],
564
- });
565
- expect(parseBodies(write).at(-1)).toMatchObject({
566
- event: "stop_failure",
567
- error_type: "aborted",
568
- query: "prompt aborted",
569
- response: "provider cancelled stream",
570
- });
571
- });
572
-
573
- it("suppresses stop OSC when agent_end willContinue", () => {
574
- enableWarpProtocol();
575
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
576
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
577
- const handlers = createHandlers();
578
- const context = bridgeContext();
579
- const sessionStart = handlers.get("session_start") as never as (
580
- event: SessionStartEvent,
581
- context: ExtensionContext,
582
- ) => void;
583
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
584
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
585
-
586
- sessionStart({ type: "session_start" }, context);
587
- write.mockClear();
588
-
589
- messageStart(userMessageStart("retry me"));
590
- const afterSubmit = write.mock.calls.length;
591
- agentEnd({
592
- type: "agent_end",
593
- willContinue: true,
594
- messages: [
595
- {
596
- role: "assistant",
597
- content: [],
598
- stopReason: "error",
599
- errorMessage: "rate limited",
600
- } as never,
601
- ],
602
- });
603
- expect(write.mock.calls.length).toBe(afterSubmit);
604
- expect(parseBodies(write).some(body => body.event === "stop" || body.event === "stop_failure")).toBe(false);
605
-
606
- // Without the flag, the same messages still emit a terminal failure stop.
607
- write.mockClear();
608
- messageStart(userMessageStart("final error"));
609
- agentEnd({
610
- type: "agent_end",
611
- messages: [
612
- {
613
- role: "assistant",
614
- content: [],
615
- stopReason: "error",
616
- errorMessage: "rate limited",
617
- } as never,
618
- ],
619
- });
620
- expect(parseBodies(write).at(-1)).toMatchObject({
621
- event: "stop_failure",
622
- query: "final error",
623
- response: "rate limited",
624
- });
625
- });
626
-
627
- it("caps prompt queries and stop responses at 200 Unicode code points without breaking JSON", () => {
628
- enableWarpProtocol();
629
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
630
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
631
- const handlers = createHandlers();
632
- const context = bridgeContext();
633
- const sessionStart = handlers.get("session_start") as never as (
634
- event: SessionStartEvent,
635
- context: ExtensionContext,
636
- ) => void;
637
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
638
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
639
-
640
- sessionStart({ type: "session_start" }, context);
641
- write.mockClear();
642
-
643
- const query = `${"q".repeat(199)}😀tail`;
644
- const response = `${"a".repeat(199)}😀tail`;
645
- messageStart(userMessageStart(query));
646
- agentEnd({
647
- type: "agent_end",
648
- messages: [{ role: "assistant", content: [{ type: "text", text: response }] } as never],
649
- });
650
-
651
- const bodies = parseBodies(write);
652
- const promptSubmit = bodies.find(body => body.event === "prompt_submit");
653
- const stop = bodies.find(body => body.event === "stop");
654
- expect(promptSubmit?.query).toBe(`${"q".repeat(199)}😀`);
655
- expect(Array.from(promptSubmit?.query as string)).toHaveLength(200);
656
- expect(stop?.query).toBe(`${"q".repeat(199)}😀`);
657
- expect(stop?.response).toBe(`${"a".repeat(199)}😀`);
658
- expect(Array.from(stop?.response as string)).toHaveLength(200);
659
- });
660
-
661
- it("rebuilds the emitter and resets prompt state after a session switch", () => {
662
- enableWarpProtocol();
663
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
664
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
665
- const handlers = createHandlers();
666
- let sessionId = "session-old";
667
- const context = {
668
- sessionManager: {
669
- getSessionId: () => sessionId,
670
- getCwd: () => process.cwd(),
671
- },
672
- } as never as ExtensionContext;
673
- const sessionStart = handlers.get("session_start") as never as (
674
- event: SessionStartEvent,
675
- context: ExtensionContext,
676
- ) => void;
677
- const sessionSwitch = handlers.get("session_switch") as never as (
678
- event: SessionSwitchEvent,
679
- context: ExtensionContext,
680
- ) => void;
681
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
682
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
683
-
684
- sessionStart({ type: "session_start" }, context);
685
- messageStart(userMessageStart("old prompt"));
686
- sessionId = "session-new";
687
- write.mockClear();
688
-
689
- sessionSwitch({ type: "session_switch", reason: "new", previousSessionFile: undefined }, context);
690
- agentEnd({
691
- type: "agent_end",
692
- messages: [{ role: "assistant", content: [{ type: "text", text: "orphan stop" }] } as never],
693
- });
694
-
695
- const bodies = parseBodies(write);
696
- expect(bodies).toEqual([
697
- expect.objectContaining({ event: "session_start", session_id: "session-new", project }),
698
- expect.objectContaining({
699
- event: "stop",
700
- session_id: "session-new",
701
- response: "orphan stop",
702
- project,
703
- }),
704
- ]);
705
- expect(bodies[1]).not.toHaveProperty("query");
706
- });
707
-
708
- it("rebuilds the emitter and resets prompt state after a session branch", () => {
709
- enableWarpProtocol();
710
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
711
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
712
- const handlers = createHandlers();
713
- let sessionId = "session-old";
714
- const context = {
715
- sessionManager: {
716
- getSessionId: () => sessionId,
717
- getCwd: () => process.cwd(),
718
- },
719
- } as never as ExtensionContext;
720
- const sessionStart = handlers.get("session_start") as never as (
721
- event: SessionStartEvent,
722
- context: ExtensionContext,
723
- ) => void;
724
- const sessionBranch = handlers.get("session_branch") as never as (
725
- event: SessionBranchEvent,
726
- context: ExtensionContext,
727
- ) => void;
728
- const messageStart = handlers.get("message_start") as never as (event: MessageStartEvent) => void;
729
- const agentEnd = handlers.get("agent_end") as never as (event: AgentEndEvent) => void;
730
-
731
- sessionStart({ type: "session_start" }, context);
732
- messageStart(userMessageStart("old prompt"));
733
- sessionId = "session-branched";
734
- write.mockClear();
735
-
736
- sessionBranch({ type: "session_branch", previousSessionFile: undefined }, context);
737
- agentEnd({
738
- type: "agent_end",
739
- messages: [{ role: "assistant", content: [{ type: "text", text: "orphan stop" }] } as never],
740
- });
741
-
742
- const bodies = parseBodies(write);
743
- expect(bodies).toEqual([
744
- expect.objectContaining({ event: "session_start", session_id: "session-branched", project }),
745
- expect.objectContaining({
746
- event: "stop",
747
- session_id: "session-branched",
748
- response: "orphan stop",
749
- project,
750
- }),
751
- ]);
752
- expect(bodies[1]).not.toHaveProperty("query");
753
- });
754
-
755
- it("maps approval requests to Warp permission requests", () => {
756
- enableWarpProtocol();
757
- const write = vi.spyOn(process.stdout, "write").mockReturnValue(true);
758
- vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false);
759
- const handlers = createHandlers();
760
- const sessionStart = handlers.get("session_start") as never as (
761
- event: SessionStartEvent,
762
- context: ExtensionContext,
763
- ) => void;
764
- sessionStart({ type: "session_start" }, bridgeContext());
765
- write.mockClear();
766
-
767
- const approvalRequested = handlers.get("tool_approval_requested") as never as (
768
- event: ToolApprovalRequestedEvent,
769
- ) => void;
770
- approvalRequested({
771
- type: "tool_approval_requested",
772
- sessionId: "session-123",
773
- toolCallId: "tool-call-123",
774
- toolName: "bash",
775
- approvalMode: "always-ask",
776
- });
777
-
778
- const osc = write.mock.calls[0]?.[0] as string;
779
- expect(osc.startsWith(OSC_PREFIX)).toBe(true);
780
- expect(osc.endsWith("\x07")).toBe(true);
781
- const body = JSON.parse(osc.slice(OSC_PREFIX.length, osc.length - 1));
782
- expect(body).toEqual({
783
- event: "permission_request",
784
- tool_name: "bash",
785
- summary: "omp wants to run bash",
786
- v: 1,
787
- agent: "omp",
788
- session_id: "session-123",
789
- cwd: process.cwd(),
790
- project,
791
- plugin_version: VERSION,
792
- });
793
- });
794
- });