@dungle-scrubs/tallow 0.8.21 → 0.8.23

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 (217) hide show
  1. package/dist/cli.js +35 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +2 -0
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +82 -0
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/sdk.d.ts +17 -0
  10. package/dist/sdk.d.ts.map +1 -1
  11. package/dist/sdk.js +68 -1
  12. package/dist/sdk.js.map +1 -1
  13. package/dist/workspace-transition-relay.d.ts +40 -7
  14. package/dist/workspace-transition-relay.d.ts.map +1 -1
  15. package/dist/workspace-transition-relay.js +81 -16
  16. package/dist/workspace-transition-relay.js.map +1 -1
  17. package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
  18. package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
  19. package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
  20. package/extensions/_shared/atomic-write.ts +1 -1
  21. package/extensions/_shared/bordered-box.ts +102 -0
  22. package/extensions/_shared/interop-events.ts +5 -0
  23. package/extensions/_shared/pid-registry.ts +1 -1
  24. package/extensions/agent-commands-tool/index.ts +4 -1
  25. package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
  26. package/extensions/background-task-tool/index.ts +139 -221
  27. package/extensions/bash-tool-enhanced/index.ts +1 -75
  28. package/extensions/cd-tool/index.ts +2 -2
  29. package/extensions/context-fork/spawn.ts +4 -1
  30. package/extensions/health/index.ts +6 -6
  31. package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
  32. package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
  33. package/extensions/hooks/index.ts +27 -4
  34. package/extensions/loop/__tests__/loop.test.ts +168 -4
  35. package/extensions/loop/extension.json +6 -5
  36. package/extensions/loop/index.ts +242 -31
  37. package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
  38. package/extensions/plan-mode-tool/index.ts +103 -41
  39. package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
  40. package/extensions/prompt-suggestions/index.ts +41 -6
  41. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
  42. package/extensions/slash-command-bridge/extension.json +1 -1
  43. package/extensions/slash-command-bridge/index.ts +230 -116
  44. package/extensions/subagent-tool/index.ts +2 -2
  45. package/extensions/subagent-tool/process.ts +4 -5
  46. package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
  47. package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
  48. package/extensions/teams-tool/dashboard.ts +3 -5
  49. package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
  50. package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
  51. package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
  52. package/extensions/wezterm-pane-control/index.ts +113 -8
  53. package/package.json +6 -4
  54. package/packages/tallow-tui/README.md +51 -0
  55. package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
  56. package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
  57. package/packages/tallow-tui/dist/autocomplete.js +564 -0
  58. package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
  59. package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
  60. package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
  61. package/packages/tallow-tui/dist/border-styles.js +46 -0
  62. package/packages/tallow-tui/dist/border-styles.js.map +1 -0
  63. package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
  64. package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
  65. package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
  66. package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
  67. package/packages/tallow-tui/dist/components/box.d.ts +22 -0
  68. package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
  69. package/packages/tallow-tui/dist/components/box.js +104 -0
  70. package/packages/tallow-tui/dist/components/box.js.map +1 -0
  71. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
  72. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
  73. package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
  74. package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
  75. package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
  76. package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
  77. package/packages/tallow-tui/dist/components/editor.js +1766 -0
  78. package/packages/tallow-tui/dist/components/editor.js.map +1 -0
  79. package/packages/tallow-tui/dist/components/image.d.ts +126 -0
  80. package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
  81. package/packages/tallow-tui/dist/components/image.js +245 -0
  82. package/packages/tallow-tui/dist/components/image.js.map +1 -0
  83. package/packages/tallow-tui/dist/components/input.d.ts +37 -0
  84. package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
  85. package/packages/tallow-tui/dist/components/input.js +439 -0
  86. package/packages/tallow-tui/dist/components/input.js.map +1 -0
  87. package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
  88. package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
  89. package/packages/tallow-tui/dist/components/loader.js +146 -0
  90. package/packages/tallow-tui/dist/components/loader.js.map +1 -0
  91. package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
  92. package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
  93. package/packages/tallow-tui/dist/components/markdown.js +633 -0
  94. package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
  95. package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
  96. package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
  97. package/packages/tallow-tui/dist/components/select-list.js +156 -0
  98. package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
  99. package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
  100. package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
  101. package/packages/tallow-tui/dist/components/settings-list.js +189 -0
  102. package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
  103. package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
  104. package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
  105. package/packages/tallow-tui/dist/components/spacer.js +23 -0
  106. package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
  107. package/packages/tallow-tui/dist/components/text.d.ts +19 -0
  108. package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
  109. package/packages/tallow-tui/dist/components/text.js +91 -0
  110. package/packages/tallow-tui/dist/components/text.js.map +1 -0
  111. package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
  112. package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
  113. package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
  114. package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
  115. package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
  116. package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
  117. package/packages/tallow-tui/dist/editor-component.js +2 -0
  118. package/packages/tallow-tui/dist/editor-component.js.map +1 -0
  119. package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
  120. package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
  121. package/packages/tallow-tui/dist/fuzzy.js +107 -0
  122. package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
  123. package/packages/tallow-tui/dist/index.d.ts +25 -0
  124. package/packages/tallow-tui/dist/index.d.ts.map +1 -0
  125. package/packages/tallow-tui/dist/index.js +35 -0
  126. package/packages/tallow-tui/dist/index.js.map +1 -0
  127. package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
  128. package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
  129. package/packages/tallow-tui/dist/keybindings.js +114 -0
  130. package/packages/tallow-tui/dist/keybindings.js.map +1 -0
  131. package/packages/tallow-tui/dist/keys.d.ts +168 -0
  132. package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
  133. package/packages/tallow-tui/dist/keys.js +971 -0
  134. package/packages/tallow-tui/dist/keys.js.map +1 -0
  135. package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
  136. package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
  137. package/packages/tallow-tui/dist/kill-ring.js +44 -0
  138. package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
  139. package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
  140. package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
  141. package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
  142. package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
  143. package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
  144. package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
  145. package/packages/tallow-tui/dist/terminal-image.js +460 -0
  146. package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
  147. package/packages/tallow-tui/dist/terminal.d.ts +102 -0
  148. package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
  149. package/packages/tallow-tui/dist/terminal.js +263 -0
  150. package/packages/tallow-tui/dist/terminal.js.map +1 -0
  151. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
  152. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
  153. package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
  154. package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
  155. package/packages/tallow-tui/dist/tui.d.ts +239 -0
  156. package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
  157. package/packages/tallow-tui/dist/tui.js +1058 -0
  158. package/packages/tallow-tui/dist/tui.js.map +1 -0
  159. package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
  160. package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
  161. package/packages/tallow-tui/dist/undo-stack.js +25 -0
  162. package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
  163. package/packages/tallow-tui/dist/utils.d.ts +96 -0
  164. package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
  165. package/packages/tallow-tui/dist/utils.js +843 -0
  166. package/packages/tallow-tui/dist/utils.js.map +1 -0
  167. package/packages/tallow-tui/package.json +24 -0
  168. package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
  169. package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
  170. package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
  171. package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
  172. package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
  173. package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
  174. package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
  175. package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
  176. package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
  177. package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
  178. package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
  179. package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
  180. package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
  181. package/packages/tallow-tui/src/autocomplete.ts +716 -0
  182. package/packages/tallow-tui/src/border-styles.ts +60 -0
  183. package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
  184. package/packages/tallow-tui/src/components/box.ts +137 -0
  185. package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
  186. package/packages/tallow-tui/src/components/editor.ts +2143 -0
  187. package/packages/tallow-tui/src/components/image.ts +315 -0
  188. package/packages/tallow-tui/src/components/input.ts +522 -0
  189. package/packages/tallow-tui/src/components/loader.ts +187 -0
  190. package/packages/tallow-tui/src/components/markdown.ts +780 -0
  191. package/packages/tallow-tui/src/components/select-list.ts +197 -0
  192. package/packages/tallow-tui/src/components/settings-list.ts +264 -0
  193. package/packages/tallow-tui/src/components/spacer.ts +28 -0
  194. package/packages/tallow-tui/src/components/text.ts +113 -0
  195. package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
  196. package/packages/tallow-tui/src/editor-component.ts +92 -0
  197. package/packages/tallow-tui/src/fuzzy.ts +133 -0
  198. package/packages/tallow-tui/src/index.ts +118 -0
  199. package/packages/tallow-tui/src/keybindings.ts +183 -0
  200. package/packages/tallow-tui/src/keys.ts +1189 -0
  201. package/packages/tallow-tui/src/kill-ring.ts +46 -0
  202. package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
  203. package/packages/tallow-tui/src/terminal-image.ts +619 -0
  204. package/packages/tallow-tui/src/terminal.ts +350 -0
  205. package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
  206. package/packages/tallow-tui/src/tui.ts +1336 -0
  207. package/packages/tallow-tui/src/undo-stack.ts +28 -0
  208. package/packages/tallow-tui/src/utils.ts +948 -0
  209. package/packages/tallow-tui/tsconfig.build.json +21 -0
  210. package/runtime/agent-runner.ts +20 -0
  211. package/runtime/atomic-write.ts +8 -0
  212. package/runtime/otel.ts +12 -0
  213. package/runtime/resolve-module.ts +23 -0
  214. package/runtime/runtime-path-provider.ts +12 -0
  215. package/runtime/runtime-provenance.ts +17 -0
  216. package/runtime/workspace-transition-relay.ts +21 -0
  217. package/runtime/workspace-transition.ts +29 -0
@@ -0,0 +1,216 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
3
+ import { ExtensionHarness } from "../../test-utils/extension-harness.js";
4
+ import backgroundTasksExtension, {
5
+ setBackgroundTaskSpawnForTests,
6
+ } from "../background-task-tool/index.js";
7
+ import { registerTasksExtension } from "../tasks/commands/register-tasks-extension.js";
8
+ import { TaskListStore } from "../tasks/state/index.js";
9
+
10
+ const ORIGINAL_PI_IS_SUBAGENT = process.env.PI_IS_SUBAGENT;
11
+ const ORIGINAL_PI_TEAM_NAME = process.env.PI_TEAM_NAME;
12
+
13
+ interface CapturedWidget {
14
+ render: ((width: number) => string[]) | null;
15
+ }
16
+
17
+ interface WidgetCapture {
18
+ widgets: Map<string, CapturedWidget>;
19
+ }
20
+
21
+ /**
22
+ * Build an interactive-mode-like context that captures widgets by key.
23
+ *
24
+ * @param captured - Mutable widget capture registry
25
+ * @returns Extension context for event and tool execution
26
+ */
27
+ function createContext(captured: WidgetCapture): ExtensionContext {
28
+ const theme = {
29
+ bold: (text: string) => text,
30
+ fg: (_color: unknown, text: string) => text,
31
+ strikethrough: (text: string) => text,
32
+ } as ExtensionContext["ui"]["theme"];
33
+
34
+ return {
35
+ ui: {
36
+ async confirm() {
37
+ return false;
38
+ },
39
+ async custom() {
40
+ return undefined as never;
41
+ },
42
+ async editor() {
43
+ return undefined;
44
+ },
45
+ get theme() {
46
+ return theme;
47
+ },
48
+ getAllThemes() {
49
+ return [];
50
+ },
51
+ getEditorText() {
52
+ return "";
53
+ },
54
+ getTheme() {
55
+ return undefined;
56
+ },
57
+ getToolsExpanded() {
58
+ return false;
59
+ },
60
+ async input() {
61
+ return undefined;
62
+ },
63
+ notify() {},
64
+ pasteToEditor() {},
65
+ async select() {
66
+ return undefined;
67
+ },
68
+ setEditorComponent() {},
69
+ setEditorText() {},
70
+ setFooter() {},
71
+ setHeader() {},
72
+ setStatus() {},
73
+ setTheme() {
74
+ return { success: false, error: "Test stub" };
75
+ },
76
+ setTitle() {},
77
+ setToolsExpanded() {},
78
+ setWidget(name, widget) {
79
+ if (!widget) {
80
+ captured.widgets.delete(name);
81
+ return;
82
+ }
83
+ if (Array.isArray(widget)) {
84
+ captured.widgets.set(name, {
85
+ render: () => widget,
86
+ });
87
+ return;
88
+ }
89
+ const component = widget(undefined as never, undefined as never);
90
+ captured.widgets.set(name, {
91
+ render: (width) => component.render(width),
92
+ });
93
+ },
94
+ setWorkingMessage() {},
95
+ } as ExtensionContext["ui"],
96
+ hasUI: true,
97
+ cwd: process.cwd(),
98
+ sessionManager: {
99
+ appendEntry: () => {},
100
+ getEntries: () => [],
101
+ } as never,
102
+ modelRegistry: {
103
+ getApiKeyForProvider: async () => undefined,
104
+ } as never,
105
+ model: undefined,
106
+ isIdle: () => true,
107
+ abort: () => {},
108
+ hasPendingMessages: () => false,
109
+ shutdown: () => {},
110
+ getContextUsage: () => undefined,
111
+ compact: () => {},
112
+ getSystemPrompt: () => "",
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Read a registered tool by name.
118
+ *
119
+ * @param harness - Extension harness instance
120
+ * @param name - Tool name to resolve
121
+ * @returns Registered tool definition
122
+ */
123
+ function getTool(harness: ExtensionHarness, name: string): ToolDefinition {
124
+ const tool = harness.tools.get(name);
125
+ if (!tool) throw new Error(`Expected tool "${name}" to be registered`);
126
+ return tool;
127
+ }
128
+
129
+ /**
130
+ * Execute a tool with the widget-capturing context.
131
+ *
132
+ * @param ctx - Extension execution context
133
+ * @param tool - Tool definition to execute
134
+ * @param params - Tool parameters
135
+ * @returns Tool result payload
136
+ */
137
+ async function execTool(
138
+ ctx: ExtensionContext,
139
+ tool: ToolDefinition,
140
+ params: Record<string, unknown>
141
+ ): Promise<{ details: Record<string, unknown> }> {
142
+ return (await tool.execute("test-tool-call", params as never, undefined, undefined, ctx)) as {
143
+ details: Record<string, unknown>;
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Render a captured widget to plain text.
149
+ *
150
+ * @param captured - Widget capture registry
151
+ * @param name - Widget key
152
+ * @param width - Terminal width to render at
153
+ * @returns Joined widget text
154
+ */
155
+ function renderWidget(captured: WidgetCapture, name: string, width: number): string {
156
+ const widget = captured.widgets.get(name);
157
+ if (!widget?.render) throw new Error(`Expected widget "${name}" to be captured`);
158
+ return widget.render(width).join("\n");
159
+ }
160
+
161
+ beforeEach(() => {
162
+ process.env.PI_IS_SUBAGENT = "0";
163
+ delete process.env.PI_TEAM_NAME;
164
+ setBackgroundTaskSpawnForTests(undefined);
165
+ });
166
+
167
+ afterEach(() => {
168
+ setBackgroundTaskSpawnForTests(undefined);
169
+ if (ORIGINAL_PI_IS_SUBAGENT === undefined) delete process.env.PI_IS_SUBAGENT;
170
+ else process.env.PI_IS_SUBAGENT = ORIGINAL_PI_IS_SUBAGENT;
171
+ if (ORIGINAL_PI_TEAM_NAME === undefined) delete process.env.PI_TEAM_NAME;
172
+ else process.env.PI_TEAM_NAME = ORIGINAL_PI_TEAM_NAME;
173
+ });
174
+
175
+ describe("background task widget ownership", () => {
176
+ it("keeps the standalone bg-tasks widget when tasks is not registered", async () => {
177
+ const harness = ExtensionHarness.create();
178
+ const captured: WidgetCapture = { widgets: new Map() };
179
+ const ctx = createContext(captured);
180
+ backgroundTasksExtension(harness.api);
181
+
182
+ try {
183
+ await harness.fireEvent("session_start", {}, ctx);
184
+ const bgBash = getTool(harness, "bg_bash");
185
+ await execTool(ctx, bgBash, { command: "sleep 5" });
186
+
187
+ expect(captured.widgets.has("bg-tasks")).toBe(true);
188
+ expect(renderWidget(captured, "bg-tasks", 120)).toContain("sleep 5");
189
+ } finally {
190
+ await harness.fireEvent("session_shutdown", {}, ctx);
191
+ harness.reset();
192
+ }
193
+ });
194
+
195
+ it("suppresses the standalone widget when tasks owns background-task presentation", async () => {
196
+ const harness = ExtensionHarness.create();
197
+ const captured: WidgetCapture = { widgets: new Map() };
198
+ const ctx = createContext(captured);
199
+ backgroundTasksExtension(harness.api);
200
+ registerTasksExtension(harness.api, new TaskListStore(null), null);
201
+
202
+ try {
203
+ await harness.fireEvent("session_start", {}, ctx);
204
+ const bgBash = getTool(harness, "bg_bash");
205
+ await execTool(ctx, bgBash, { command: "sleep 5" });
206
+
207
+ expect(captured.widgets.has("bg-tasks")).toBe(false);
208
+ expect(captured.widgets.has("1-tasks")).toBe(true);
209
+ expect(renderWidget(captured, "1-tasks", 120)).toContain("Background Tasks (1)");
210
+ expect(renderWidget(captured, "1-tasks", 120)).toContain("sleep 5");
211
+ } finally {
212
+ await harness.fireEvent("session_shutdown", {}, ctx);
213
+ harness.reset();
214
+ }
215
+ });
216
+ });
@@ -192,3 +192,159 @@ describe("Claude hooks compatibility integration", () => {
192
192
  expect(handlers).not.toContain("echo project-claude");
193
193
  });
194
194
  });
195
+
196
+ describe("Package hooks with Claude format", () => {
197
+ it("translates Claude event names in package hooks.json", () => {
198
+ const pkgDir = join(cwd, "my-package");
199
+ writeJson(join(pkgDir, "hooks.json"), {
200
+ PreToolUse: [
201
+ {
202
+ matcher: "Bash",
203
+ hooks: [{ type: "command", command: "echo pkg-pre" }],
204
+ },
205
+ ],
206
+ Stop: [
207
+ {
208
+ hooks: [{ type: "command", command: "echo pkg-stop" }],
209
+ },
210
+ ],
211
+ });
212
+
213
+ writeJson(join(homeDir, ".tallow", "settings.json"), {
214
+ packages: [pkgDir],
215
+ });
216
+
217
+ const config = loadHooksConfig(cwd);
218
+ expect(config.tool_call).toHaveLength(1);
219
+ expect(config.tool_call[0]?.matcher).toBe("bash");
220
+ expect(config.tool_call[0]?.hooks[0]?.command).toBe("echo pkg-pre");
221
+ expect(config.tool_call[0]?.hooks[0]?._claudeSource).toBe(true);
222
+ expect(config.tool_call[0]?.hooks[0]?._claudeEventName).toBe("PreToolUse");
223
+ expect(config.agent_end).toHaveLength(1);
224
+ expect(config.agent_end[0]?.hooks[0]?.command).toBe("echo pkg-stop");
225
+ });
226
+
227
+ it("does not double-translate native tallow hooks in packages", () => {
228
+ const pkgDir = join(cwd, "native-package");
229
+ writeJson(join(pkgDir, "hooks.json"), {
230
+ tool_call: [
231
+ {
232
+ matcher: "bash",
233
+ hooks: [{ type: "command", command: "echo native" }],
234
+ },
235
+ ],
236
+ });
237
+
238
+ writeJson(join(homeDir, ".tallow", "settings.json"), {
239
+ packages: [pkgDir],
240
+ });
241
+
242
+ const config = loadHooksConfig(cwd);
243
+ expect(config.tool_call).toHaveLength(1);
244
+ expect(config.tool_call[0]?.matcher).toBe("bash");
245
+ expect(config.tool_call[0]?.hooks[0]?.command).toBe("echo native");
246
+ expect(config.tool_call[0]?.hooks[0]?._claudeSource).toBeUndefined();
247
+ });
248
+
249
+ it("handles mixed Claude and native events in a package hooks.json", () => {
250
+ const pkgDir = join(cwd, "mixed-package");
251
+ writeJson(join(pkgDir, "hooks.json"), {
252
+ PreToolUse: [
253
+ {
254
+ matcher: "Edit|Write",
255
+ hooks: [{ type: "command", command: "echo claude-pre" }],
256
+ },
257
+ ],
258
+ tool_call: [
259
+ {
260
+ matcher: "bash",
261
+ hooks: [{ type: "command", command: "echo native-tool" }],
262
+ },
263
+ ],
264
+ });
265
+
266
+ writeJson(join(homeDir, ".tallow", "settings.json"), {
267
+ packages: [pkgDir],
268
+ });
269
+
270
+ const config = loadHooksConfig(cwd);
271
+ expect(config.tool_call).toHaveLength(2);
272
+ const commands = config.tool_call.map((entry) => entry.hooks[0]?.command);
273
+ expect(commands).toContain("echo claude-pre");
274
+ expect(commands).toContain("echo native-tool");
275
+ });
276
+
277
+ it("translates Claude hooks from project-level package settings", () => {
278
+ const pkgDir = join(cwd, "proj-pkg");
279
+ writeJson(join(pkgDir, "hooks.json"), {
280
+ UserPromptSubmit: [
281
+ {
282
+ hooks: [{ type: "command", command: "echo proj-input" }],
283
+ },
284
+ ],
285
+ });
286
+
287
+ writeJson(join(cwd, ".tallow", "settings.json"), {
288
+ packages: [pkgDir],
289
+ });
290
+
291
+ const config = loadHooksConfig(cwd);
292
+ expect(config.input).toHaveLength(1);
293
+ expect(config.input[0]?.hooks[0]?.command).toBe("echo proj-input");
294
+ expect(config.input[0]?.hooks[0]?._claudeEventName).toBe("UserPromptSubmit");
295
+ });
296
+
297
+ it("blocks untrusted project package hooks with Claude format", () => {
298
+ const pkgDir = join(cwd, "untrusted-pkg");
299
+ writeJson(join(pkgDir, "hooks.json"), {
300
+ PreToolUse: [
301
+ {
302
+ matcher: "Bash",
303
+ hooks: [{ type: "command", command: "echo untrusted" }],
304
+ },
305
+ ],
306
+ });
307
+
308
+ writeJson(join(cwd, ".tallow", "settings.json"), {
309
+ packages: [pkgDir],
310
+ });
311
+
312
+ process.env.TALLOW_PROJECT_TRUST_STATUS = "untrusted";
313
+ const config = loadHooksConfig(cwd);
314
+ expect(config.tool_call ?? []).toHaveLength(0);
315
+ });
316
+ });
317
+
318
+ describe("Extension hooks with Claude format", () => {
319
+ it("translates Claude event names in extension hooks.json", () => {
320
+ writeJson(join(homeDir, ".tallow", "extensions", "my-ext", "hooks.json"), {
321
+ PreToolUse: [
322
+ {
323
+ matcher: "Bash",
324
+ hooks: [{ type: "command", command: "echo ext-pre" }],
325
+ },
326
+ ],
327
+ });
328
+
329
+ const config = loadHooksConfig(cwd);
330
+ expect(config.tool_call).toHaveLength(1);
331
+ expect(config.tool_call[0]?.matcher).toBe("bash");
332
+ expect(config.tool_call[0]?.hooks[0]?._claudeSource).toBe(true);
333
+ });
334
+
335
+ it("translates Claude hooks in project extension hooks.json", () => {
336
+ writeJson(join(cwd, ".tallow", "extensions", "proj-ext", "hooks.json"), {
337
+ PostToolUse: [
338
+ {
339
+ matcher: "Write",
340
+ hooks: [{ type: "command", command: "echo proj-ext-post" }],
341
+ },
342
+ ],
343
+ });
344
+
345
+ const config = loadHooksConfig(cwd);
346
+ expect(config.tool_result).toHaveLength(1);
347
+ expect(config.tool_result[0]?.matcher).toBe("write");
348
+ expect(config.tool_result[0]?.hooks[0]?._claudeEventName).toBe("PostToolUse");
349
+ });
350
+ });
@@ -1,23 +1,144 @@
1
1
  /**
2
- * Integration test for slash-command-bridge.
2
+ * Integration tests for slash-command-bridge.
3
3
  *
4
- * Verifies the tool works end-to-end via a session runner with a mock model
5
- * that invokes the run_slash_command tool.
4
+ * The compact regression uses the real headless session path and verifies the
5
+ * ordered lifecycle from tool result → deferred compact → resumed turn.
6
6
  */
7
7
 
8
- import { afterEach, describe, expect, it } from "bun:test";
8
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
9
+ import type { AgentMessage } from "@mariozechner/pi-ai";
9
10
  import type { ExtensionAPI, ExtensionFactory } from "@mariozechner/pi-coding-agent";
11
+ import { ManualTimerScheduler } from "../../test-utils/manual-timer-scheduler.js";
10
12
  import { createScriptedStreamFn } from "../../test-utils/mock-model.js";
11
13
  import { createSessionRunner, type SessionRunner } from "../../test-utils/session-runner.js";
12
- import slashCommandBridge from "../slash-command-bridge/index.js";
14
+ import slashCommandBridge, {
15
+ resetSlashCommandBridgeStateForTests,
16
+ setSlashCommandBridgeSchedulerForTests,
17
+ } from "../slash-command-bridge/index.js";
18
+
19
+ interface CompactionTrackerState {
20
+ order: string[];
21
+ resumedAssistantCount: number;
22
+ }
13
23
 
14
24
  let runner: SessionRunner | undefined;
25
+ let scheduler: ManualTimerScheduler;
26
+
27
+ beforeEach(() => {
28
+ scheduler = new ManualTimerScheduler();
29
+ setSlashCommandBridgeSchedulerForTests(scheduler.runtime);
30
+ });
15
31
 
16
32
  afterEach(() => {
17
33
  runner?.dispose();
18
34
  runner = undefined;
35
+ resetSlashCommandBridgeStateForTests();
19
36
  });
20
37
 
38
+ /**
39
+ * Returns assistant text content as a plain string for matcher-friendly assertions.
40
+ *
41
+ * @param message - Agent message to inspect
42
+ * @returns Flattened text content
43
+ */
44
+ function getMessageText(message: AgentMessage): string {
45
+ if (typeof message.content === "string") {
46
+ return message.content;
47
+ }
48
+
49
+ return message.content
50
+ .filter((part) => part.type === "text")
51
+ .map((part) => part.text)
52
+ .join("\n");
53
+ }
54
+
55
+ /**
56
+ * Builds a tracking extension that records the compact lifecycle order.
57
+ *
58
+ * The hook also provides a deterministic compaction result so the regression can
59
+ * exercise the real deferred path without making a network summarization call.
60
+ *
61
+ * @param state - Shared mutable tracker state for assertions
62
+ * @returns Extension factory that records compact lifecycle events
63
+ */
64
+ function buildCompactionTracker(state: CompactionTrackerState): ExtensionFactory {
65
+ return (pi: ExtensionAPI): void => {
66
+ pi.on("tool_result", async (event) => {
67
+ if (event.toolName !== "run_slash_command") {
68
+ return;
69
+ }
70
+ if ((event.details as { command?: string } | undefined)?.command !== "compact") {
71
+ return;
72
+ }
73
+ state.order.push("tool_result");
74
+ });
75
+
76
+ pi.on("turn_end", async (event) => {
77
+ if (event.message.role !== "assistant") {
78
+ return;
79
+ }
80
+ state.order.push(`turn_end:${event.message.stopReason}`);
81
+ });
82
+
83
+ pi.on("session_before_compact", async () => {
84
+ state.order.push("session_before_compact");
85
+ return {
86
+ compaction: {
87
+ summary: "mock compact summary",
88
+ firstKeptEntryId: undefined,
89
+ tokensBefore: 123,
90
+ details: { modifiedFiles: [], readFiles: [] },
91
+ },
92
+ };
93
+ });
94
+
95
+ pi.on("session_compact", async () => {
96
+ state.order.push("session_compact");
97
+ });
98
+
99
+ pi.on("message_end", async (event) => {
100
+ const text = getMessageText(event.message);
101
+ if (event.message.role === "custom" && text.includes("Session compaction is complete")) {
102
+ state.order.push("continuation_message");
103
+ }
104
+ if (event.message.role === "assistant" && text.includes("resumed after compact")) {
105
+ state.order.push("assistant_resumed");
106
+ state.resumedAssistantCount++;
107
+ }
108
+ });
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Lets queued extension/session work settle after a prompt or timer advance.
114
+ *
115
+ * `session.prompt()` does not wait for all extension-side follow-up work, so the
116
+ * regression explicitly drains microtasks around `agent.waitForIdle()`.
117
+ *
118
+ * @param activeRunner - Runner whose session should be drained
119
+ * @returns Nothing
120
+ */
121
+ async function flushSessionWork(activeRunner: SessionRunner): Promise<void> {
122
+ await Promise.resolve();
123
+ await Promise.resolve();
124
+ await activeRunner.session.agent.waitForIdle();
125
+ await Promise.resolve();
126
+ await Promise.resolve();
127
+ await activeRunner.session.agent.waitForIdle();
128
+ await Promise.resolve();
129
+ }
130
+
131
+ /**
132
+ * Returns the first index of a recorded lifecycle step.
133
+ *
134
+ * @param order - Recorded lifecycle events
135
+ * @param step - Step name to locate
136
+ * @returns Zero-based index in the order array
137
+ */
138
+ function indexOfStep(order: readonly string[], step: string): number {
139
+ return order.indexOf(step);
140
+ }
141
+
21
142
  describe("slash-command-bridge integration", () => {
22
143
  it("model invokes show-system-prompt and receives prompt text", async () => {
23
144
  const toolResults: string[] = [];
@@ -44,12 +165,11 @@ describe("slash-command-bridge integration", () => {
44
165
  await runner.run("Show me the system prompt");
45
166
 
46
167
  expect(toolResults).toHaveLength(1);
47
- // System prompt exists and is non-empty in a real session
48
168
  expect(toolResults[0].length).toBeGreaterThan(0);
49
169
  });
50
170
 
51
171
  it("model invokes context and receives usage data", async () => {
52
- const toolResults: Array<{ text: string; isError: boolean }> = [];
172
+ const toolResults: Array<{ isError: boolean; text: string }> = [];
53
173
 
54
174
  const tracker: ExtensionFactory = (pi: ExtensionAPI): void => {
55
175
  pi.on("tool_result", async (event) => {
@@ -75,7 +195,6 @@ describe("slash-command-bridge integration", () => {
75
195
  await runner.run("Check context usage");
76
196
 
77
197
  expect(toolResults).toHaveLength(1);
78
- // Context usage should contain token info (may be actual data or "no data" error)
79
198
  expect(toolResults[0].text.length).toBeGreaterThan(0);
80
199
  });
81
200
 
@@ -108,31 +227,58 @@ describe("slash-command-bridge integration", () => {
108
227
  expect(toolResults[0]).toContain("reboot");
109
228
  });
110
229
 
111
- it("model invokes compact successfully", async () => {
112
- const toolResults: string[] = [];
113
-
114
- const tracker: ExtensionFactory = (pi: ExtensionAPI): void => {
115
- pi.on("tool_result", async (event) => {
116
- if (event.toolName === "run_slash_command") {
117
- const text = event.content.find((c) => c.type === "text");
118
- if (text?.type === "text") toolResults.push(text.text);
119
- }
120
- });
230
+ it("model-invoked compact preserves ordered lifecycle and resumes once", async () => {
231
+ const state: CompactionTrackerState = {
232
+ order: [],
233
+ resumedAssistantCount: 0,
121
234
  };
122
235
 
123
236
  runner = await createSessionRunner({
124
237
  streamFn: createScriptedStreamFn([
238
+ { text: "warmup" },
125
239
  {
126
240
  toolCalls: [{ name: "run_slash_command", arguments: { command: "compact" } }],
127
241
  },
128
- { text: "Compaction started" },
242
+ { text: "finish response" },
243
+ { text: "resumed after compact" },
129
244
  ]),
130
- extensionFactories: [slashCommandBridge, tracker],
245
+ extensionFactories: [slashCommandBridge, buildCompactionTracker(state)],
246
+ settings: {
247
+ compaction: {
248
+ enabled: true,
249
+ keepRecentTokens: 1,
250
+ reserveTokens: 10,
251
+ },
252
+ },
131
253
  });
132
254
 
133
- await runner.run("Compact the session");
255
+ await runner.run("warm up the session");
256
+ state.order.length = 0;
257
+ state.resumedAssistantCount = 0;
134
258
 
135
- expect(toolResults).toHaveLength(1);
136
- expect(toolResults[0]).toContain("compaction will begin");
259
+ await runner.run("compact the session");
260
+ scheduler.advanceBy(200);
261
+ await flushSessionWork(runner);
262
+
263
+ expect(state.resumedAssistantCount).toBe(1);
264
+ expect(state.order.filter((step) => step === "session_before_compact")).toHaveLength(1);
265
+ expect(state.order.filter((step) => step === "session_compact")).toHaveLength(1);
266
+ expect(state.order.filter((step) => step === "assistant_resumed")).toHaveLength(1);
267
+
268
+ const toolResultIndex = indexOfStep(state.order, "tool_result");
269
+ const toolUseTurnEndIndex = indexOfStep(state.order, "turn_end:toolUse");
270
+ const finalTurnEndIndex = indexOfStep(state.order, "turn_end:stop");
271
+ const beforeCompactIndex = indexOfStep(state.order, "session_before_compact");
272
+ const compactIndex = indexOfStep(state.order, "session_compact");
273
+ const continuationIndex = indexOfStep(state.order, "continuation_message");
274
+ const resumedIndex = indexOfStep(state.order, "assistant_resumed");
275
+
276
+ expect(toolResultIndex).toBeGreaterThanOrEqual(0);
277
+ expect(toolUseTurnEndIndex).toBeGreaterThan(toolResultIndex);
278
+ expect(finalTurnEndIndex).toBeGreaterThan(toolUseTurnEndIndex);
279
+ expect(beforeCompactIndex).toBeGreaterThan(finalTurnEndIndex);
280
+ expect(compactIndex).toBeGreaterThan(beforeCompactIndex);
281
+ expect(continuationIndex).toBeGreaterThan(compactIndex);
282
+ expect(resumedIndex).toBeGreaterThan(continuationIndex);
137
283
  });
138
284
  });
@@ -3,4 +3,4 @@
3
3
  *
4
4
  * Extensions should import from here rather than reaching into `src/`.
5
5
  */
6
- export { atomicWriteFileSync, restoreFromBackup } from "../../src/atomic-write.js";
6
+ export { atomicWriteFileSync, restoreFromBackup } from "../../runtime/atomic-write.js";