@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.
- package/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +2 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +82 -0
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +68 -1
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-relay.d.ts +40 -7
- package/dist/workspace-transition-relay.d.ts.map +1 -1
- package/dist/workspace-transition-relay.js +81 -16
- package/dist/workspace-transition-relay.js.map +1 -1
- package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
- package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
- package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
- package/extensions/_shared/atomic-write.ts +1 -1
- package/extensions/_shared/bordered-box.ts +102 -0
- package/extensions/_shared/interop-events.ts +5 -0
- package/extensions/_shared/pid-registry.ts +1 -1
- package/extensions/agent-commands-tool/index.ts +4 -1
- package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
- package/extensions/background-task-tool/index.ts +139 -221
- package/extensions/bash-tool-enhanced/index.ts +1 -75
- package/extensions/cd-tool/index.ts +2 -2
- package/extensions/context-fork/spawn.ts +4 -1
- package/extensions/health/index.ts +6 -6
- package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
- package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
- package/extensions/hooks/index.ts +27 -4
- package/extensions/loop/__tests__/loop.test.ts +168 -4
- package/extensions/loop/extension.json +6 -5
- package/extensions/loop/index.ts +242 -31
- package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
- package/extensions/plan-mode-tool/index.ts +103 -41
- package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
- package/extensions/prompt-suggestions/index.ts +41 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
- package/extensions/slash-command-bridge/extension.json +1 -1
- package/extensions/slash-command-bridge/index.ts +230 -116
- package/extensions/subagent-tool/index.ts +2 -2
- package/extensions/subagent-tool/process.ts +4 -5
- package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
- package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
- package/extensions/teams-tool/dashboard.ts +3 -5
- package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
- package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
- package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
- package/extensions/wezterm-pane-control/index.ts +113 -8
- package/package.json +6 -4
- package/packages/tallow-tui/README.md +51 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
- package/packages/tallow-tui/dist/autocomplete.js +564 -0
- package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
- package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
- package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
- package/packages/tallow-tui/dist/border-styles.js +46 -0
- package/packages/tallow-tui/dist/border-styles.js.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
- package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
- package/packages/tallow-tui/dist/components/box.d.ts +22 -0
- package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/box.js +104 -0
- package/packages/tallow-tui/dist/components/box.js.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
- package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/editor.js +1766 -0
- package/packages/tallow-tui/dist/components/editor.js.map +1 -0
- package/packages/tallow-tui/dist/components/image.d.ts +126 -0
- package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/image.js +245 -0
- package/packages/tallow-tui/dist/components/image.js.map +1 -0
- package/packages/tallow-tui/dist/components/input.d.ts +37 -0
- package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/input.js +439 -0
- package/packages/tallow-tui/dist/components/input.js.map +1 -0
- package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
- package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/loader.js +146 -0
- package/packages/tallow-tui/dist/components/loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.js +633 -0
- package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.js +156 -0
- package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.js +189 -0
- package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.js +23 -0
- package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
- package/packages/tallow-tui/dist/components/text.d.ts +19 -0
- package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/text.js +91 -0
- package/packages/tallow-tui/dist/components/text.js.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
- package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
- package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
- package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
- package/packages/tallow-tui/dist/editor-component.js +2 -0
- package/packages/tallow-tui/dist/editor-component.js.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.js +107 -0
- package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
- package/packages/tallow-tui/dist/index.d.ts +25 -0
- package/packages/tallow-tui/dist/index.d.ts.map +1 -0
- package/packages/tallow-tui/dist/index.js +35 -0
- package/packages/tallow-tui/dist/index.js.map +1 -0
- package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
- package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keybindings.js +114 -0
- package/packages/tallow-tui/dist/keybindings.js.map +1 -0
- package/packages/tallow-tui/dist/keys.d.ts +168 -0
- package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keys.js +971 -0
- package/packages/tallow-tui/dist/keys.js.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.js +44 -0
- package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
- package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.js +460 -0
- package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
- package/packages/tallow-tui/dist/terminal.d.ts +102 -0
- package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal.js +263 -0
- package/packages/tallow-tui/dist/terminal.js.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
- package/packages/tallow-tui/dist/tui.d.ts +239 -0
- package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
- package/packages/tallow-tui/dist/tui.js +1058 -0
- package/packages/tallow-tui/dist/tui.js.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.js +25 -0
- package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
- package/packages/tallow-tui/dist/utils.d.ts +96 -0
- package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
- package/packages/tallow-tui/dist/utils.js +843 -0
- package/packages/tallow-tui/dist/utils.js.map +1 -0
- package/packages/tallow-tui/package.json +24 -0
- package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
- package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
- package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
- package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
- package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
- package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
- package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
- package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
- package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
- package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
- package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
- package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
- package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
- package/packages/tallow-tui/src/autocomplete.ts +716 -0
- package/packages/tallow-tui/src/border-styles.ts +60 -0
- package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
- package/packages/tallow-tui/src/components/box.ts +137 -0
- package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
- package/packages/tallow-tui/src/components/editor.ts +2143 -0
- package/packages/tallow-tui/src/components/image.ts +315 -0
- package/packages/tallow-tui/src/components/input.ts +522 -0
- package/packages/tallow-tui/src/components/loader.ts +187 -0
- package/packages/tallow-tui/src/components/markdown.ts +780 -0
- package/packages/tallow-tui/src/components/select-list.ts +197 -0
- package/packages/tallow-tui/src/components/settings-list.ts +264 -0
- package/packages/tallow-tui/src/components/spacer.ts +28 -0
- package/packages/tallow-tui/src/components/text.ts +113 -0
- package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
- package/packages/tallow-tui/src/editor-component.ts +92 -0
- package/packages/tallow-tui/src/fuzzy.ts +133 -0
- package/packages/tallow-tui/src/index.ts +118 -0
- package/packages/tallow-tui/src/keybindings.ts +183 -0
- package/packages/tallow-tui/src/keys.ts +1189 -0
- package/packages/tallow-tui/src/kill-ring.ts +46 -0
- package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
- package/packages/tallow-tui/src/terminal-image.ts +619 -0
- package/packages/tallow-tui/src/terminal.ts +350 -0
- package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
- package/packages/tallow-tui/src/tui.ts +1336 -0
- package/packages/tallow-tui/src/undo-stack.ts +28 -0
- package/packages/tallow-tui/src/utils.ts +948 -0
- package/packages/tallow-tui/tsconfig.build.json +21 -0
- package/runtime/agent-runner.ts +20 -0
- package/runtime/atomic-write.ts +8 -0
- package/runtime/otel.ts +12 -0
- package/runtime/resolve-module.ts +23 -0
- package/runtime/runtime-path-provider.ts +12 -0
- package/runtime/runtime-provenance.ts +17 -0
- package/runtime/workspace-transition-relay.ts +21 -0
- 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
|
|
2
|
+
* Integration tests for slash-command-bridge.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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<{
|
|
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
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
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: "
|
|
242
|
+
{ text: "finish response" },
|
|
243
|
+
{ text: "resumed after compact" },
|
|
129
244
|
]),
|
|
130
|
-
extensionFactories: [slashCommandBridge,
|
|
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("
|
|
255
|
+
await runner.run("warm up the session");
|
|
256
|
+
state.order.length = 0;
|
|
257
|
+
state.resumedAssistantCount = 0;
|
|
134
258
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
});
|