@dungle-scrubs/tallow 0.9.4 → 0.9.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.
- package/dist/cli.js +7 -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 +24 -12
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +229 -146
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/interactive-reset.d.ts +49 -0
- package/dist/interactive-reset.d.ts.map +1 -0
- package/dist/interactive-reset.js +40 -0
- package/dist/interactive-reset.js.map +1 -0
- package/dist/pi-tui-editor-patch.d.ts +10 -0
- package/dist/pi-tui-editor-patch.d.ts.map +1 -0
- package/dist/pi-tui-editor-patch.js +159 -0
- package/dist/pi-tui-editor-patch.js.map +1 -0
- package/dist/pi-tui-patch.d.ts +2 -0
- package/dist/pi-tui-patch.d.ts.map +1 -0
- package/dist/pi-tui-patch.js +563 -0
- package/dist/pi-tui-patch.js.map +1 -0
- package/dist/pi-tui-settings-list-patch.d.ts +11 -0
- package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
- package/dist/pi-tui-settings-list-patch.js +38 -0
- package/dist/pi-tui-settings-list-patch.js.map +1 -0
- package/dist/reset-diagnostics.d.ts +69 -0
- package/dist/reset-diagnostics.d.ts.map +1 -0
- package/dist/reset-diagnostics.js +41 -0
- package/dist/reset-diagnostics.js.map +1 -0
- package/dist/sdk.d.ts +5 -21
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +180 -149
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts +1 -0
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +7 -17
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +4 -5
- package/extensions/_icons/index.ts +2 -4
- package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
- package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
- package/extensions/_shared/image-metadata.ts +99 -0
- package/extensions/_shared/inline-preview.ts +1 -1
- package/extensions/_shared/terminal-links.ts +22 -0
- package/extensions/ask-user-question-tool/index.ts +0 -3
- package/extensions/clear/__tests__/clear.test.ts +269 -2
- package/extensions/command-expansion/index.ts +1 -1
- package/extensions/context-files/index.ts +5 -1
- package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
- package/extensions/context-fork/extension.json +1 -1
- package/extensions/context-fork/index.ts +32 -0
- package/extensions/edit-tool-enhanced/index.ts +2 -1
- package/extensions/hooks/index.ts +33 -11
- package/extensions/loop/index.ts +14 -1
- package/extensions/lsp/index.ts +64 -13
- package/extensions/lsp/package.json +2 -2
- package/extensions/random-spinner/index.ts +7 -642
- package/extensions/read-tool-enhanced/index.ts +6 -8
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +2 -3
- package/extensions/render-stabilizer/index.ts +6 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
- package/extensions/slash-command-bridge/index.ts +14 -2
- package/extensions/subagent-tool/model-resolver.ts +274 -7
- package/extensions/tasks/commands/register-tasks-extension.ts +9 -9
- package/extensions/teams-tool/tools/register-extension.ts +1 -3
- package/extensions/web-search-tool/index.ts +2 -1
- package/extensions/write-tool-enhanced/index.ts +2 -1
- package/node_modules/@mariozechner/pi-tui/README.md +56 -34
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +1 -25
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +13 -50
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
- package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +6 -6
- package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
- package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
- package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
- package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
- package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
- package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
- package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
- package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
- package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
- package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
- package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
- package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +12 -51
- package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
- package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
- package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
- package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
- package/package.json +13 -13
- package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
- package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
- package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
- package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
- package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
- package/packages/tallow-tui/node_modules/marked/README.md +5 -4
- package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
- package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
- package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
- package/packages/tallow-tui/node_modules/marked/package.json +26 -34
- package/skills/tallow-expert/SKILL.md +1 -3
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
- package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
- package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
- package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
- package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
- package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
|
@@ -1,7 +1,140 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { AssistantMessage, ToolResultMessage, Usage } from "@mariozechner/pi-ai";
|
|
6
|
+
import type {
|
|
7
|
+
ExtensionAPI,
|
|
8
|
+
ExtensionContext,
|
|
9
|
+
ExtensionUIContext,
|
|
10
|
+
TurnEndEvent,
|
|
11
|
+
} from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
getResetDiagnosticsForTests,
|
|
14
|
+
resetResetDiagnosticsForTests,
|
|
15
|
+
} from "../../../src/reset-diagnostics.js";
|
|
16
|
+
import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
|
|
17
|
+
import { ManualTimerScheduler } from "../../../test-utils/manual-timer-scheduler.js";
|
|
18
|
+
import { registerContextForkExtension } from "../../context-fork/index.js";
|
|
19
|
+
import slashCommandBridge, {
|
|
20
|
+
resetSlashCommandBridgeStateForTests,
|
|
21
|
+
setSlashCommandBridgeSchedulerForTests,
|
|
22
|
+
} from "../../slash-command-bridge/index.js";
|
|
3
23
|
import registerClear from "../index.js";
|
|
4
24
|
|
|
25
|
+
const ZERO_USAGE: Usage = {
|
|
26
|
+
input: 0,
|
|
27
|
+
output: 0,
|
|
28
|
+
cacheRead: 0,
|
|
29
|
+
cacheWrite: 0,
|
|
30
|
+
totalTokens: 0,
|
|
31
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let harness: ExtensionHarness;
|
|
35
|
+
let scheduler: ManualTimerScheduler;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
scheduler = new ManualTimerScheduler();
|
|
39
|
+
setSlashCommandBridgeSchedulerForTests(scheduler.runtime);
|
|
40
|
+
harness = ExtensionHarness.create();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
resetResetDiagnosticsForTests();
|
|
45
|
+
resetSlashCommandBridgeStateForTests();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a compact-lifecycle test context.
|
|
50
|
+
*
|
|
51
|
+
* @param overrides - Context overrides
|
|
52
|
+
* @returns Extension context
|
|
53
|
+
*/
|
|
54
|
+
function buildContext(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
55
|
+
return {
|
|
56
|
+
ui: {} as ExtensionContext["ui"],
|
|
57
|
+
hasUI: false,
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
sessionManager: {} as ExtensionContext["sessionManager"],
|
|
60
|
+
modelRegistry: {} as ExtensionContext["modelRegistry"],
|
|
61
|
+
model: undefined,
|
|
62
|
+
isIdle: () => true,
|
|
63
|
+
abort: () => {},
|
|
64
|
+
hasPendingMessages: () => false,
|
|
65
|
+
shutdown: () => {},
|
|
66
|
+
getContextUsage: () => ({ contextWindow: 100, tokens: 90 }),
|
|
67
|
+
compact: () => {},
|
|
68
|
+
getSystemPrompt: () => "",
|
|
69
|
+
...overrides,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a realistic assistant turn_end event for compact lifecycle tests.
|
|
75
|
+
*
|
|
76
|
+
* @param stopReason - Assistant stop reason for the completed turn
|
|
77
|
+
* @returns TurnEnd event payload
|
|
78
|
+
*/
|
|
79
|
+
function buildAssistantTurnEnd(stopReason: AssistantMessage["stopReason"]): TurnEndEvent {
|
|
80
|
+
return {
|
|
81
|
+
type: "turn_end",
|
|
82
|
+
turnIndex: 0,
|
|
83
|
+
message: {
|
|
84
|
+
role: "assistant",
|
|
85
|
+
content: [],
|
|
86
|
+
api: "anthropic-messages",
|
|
87
|
+
provider: "mock",
|
|
88
|
+
model: "mock-model",
|
|
89
|
+
stopReason,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
usage: { ...ZERO_USAGE },
|
|
92
|
+
},
|
|
93
|
+
toolResults: stopReason === "toolUse" ? [buildCompactToolResult()] : [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the compact tool result payload recorded on the tool-use turn.
|
|
99
|
+
*
|
|
100
|
+
* @returns Tool result message for `run_slash_command({ command: "compact" })`
|
|
101
|
+
*/
|
|
102
|
+
function buildCompactToolResult(): ToolResultMessage<{ command: string }> {
|
|
103
|
+
return {
|
|
104
|
+
role: "toolResult",
|
|
105
|
+
toolCallId: "mock-tool-call",
|
|
106
|
+
toolName: "run_slash_command",
|
|
107
|
+
content: [
|
|
108
|
+
{ type: "text", text: "Session compaction will begin after this response completes." },
|
|
109
|
+
],
|
|
110
|
+
details: { command: "compact" },
|
|
111
|
+
isError: false,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface Deferred<T> {
|
|
117
|
+
readonly promise: Promise<T>;
|
|
118
|
+
readonly reject: (error?: unknown) => void;
|
|
119
|
+
readonly resolve: (value: T) => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create a deferred promise for controlling async completion timing in tests.
|
|
124
|
+
*
|
|
125
|
+
* @template T
|
|
126
|
+
* @returns Deferred promise controls
|
|
127
|
+
*/
|
|
128
|
+
function createDeferred<T>(): Deferred<T> {
|
|
129
|
+
let reject!: (error?: unknown) => void;
|
|
130
|
+
let resolve!: (value: T) => void;
|
|
131
|
+
const promise = new Promise<T>((innerResolve, innerReject) => {
|
|
132
|
+
resolve = innerResolve;
|
|
133
|
+
reject = innerReject;
|
|
134
|
+
});
|
|
135
|
+
return { promise, reject, resolve };
|
|
136
|
+
}
|
|
137
|
+
|
|
5
138
|
describe("clear extension", () => {
|
|
6
139
|
test("registers /clear command", () => {
|
|
7
140
|
const commands: Array<{ name: string; description: string }> = [];
|
|
@@ -35,4 +168,138 @@ describe("clear extension", () => {
|
|
|
35
168
|
await handler?.("", { newSession });
|
|
36
169
|
expect(newSession).toHaveBeenCalledTimes(1);
|
|
37
170
|
});
|
|
171
|
+
|
|
172
|
+
test("/clear cancels pending compact continuation before it can restart work", async () => {
|
|
173
|
+
await harness.loadExtension(slashCommandBridge);
|
|
174
|
+
registerClear(harness.api);
|
|
175
|
+
|
|
176
|
+
const compactTool = harness.tools.get("run_slash_command");
|
|
177
|
+
const clearCommand = harness.commands.get("clear");
|
|
178
|
+
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
179
|
+
const widgetUpdates: Array<{ key: string; content: string[] | undefined }> = [];
|
|
180
|
+
const workingMessages: Array<string | undefined> = [];
|
|
181
|
+
const ctx = buildContext({
|
|
182
|
+
hasUI: true,
|
|
183
|
+
ui: {
|
|
184
|
+
setWidget: (key: string, content?: string[]) => {
|
|
185
|
+
widgetUpdates.push({ key, content });
|
|
186
|
+
},
|
|
187
|
+
setWorkingMessage: (message?: string) => {
|
|
188
|
+
workingMessages.push(message);
|
|
189
|
+
},
|
|
190
|
+
} as ExtensionUIContext,
|
|
191
|
+
compact: (options) => {
|
|
192
|
+
compactOptions = options;
|
|
193
|
+
},
|
|
194
|
+
isIdle: () => true,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!compactTool?.execute || !clearCommand?.handler) {
|
|
198
|
+
throw new Error("expected compact tool and clear command to be registered");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await compactTool.execute("test-call-id", { command: "compact" }, undefined, undefined, ctx);
|
|
202
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
203
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
204
|
+
compactOptions?.onComplete?.();
|
|
205
|
+
|
|
206
|
+
const newSession = mock(async () => {
|
|
207
|
+
await harness.fireEvent(
|
|
208
|
+
"session_before_switch",
|
|
209
|
+
{ type: "session_before_switch", reason: "new" },
|
|
210
|
+
ctx
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
await clearCommand.handler("", { ...ctx, newSession } as never);
|
|
214
|
+
scheduler.advanceBy(200);
|
|
215
|
+
|
|
216
|
+
expect(newSession).toHaveBeenCalledTimes(1);
|
|
217
|
+
expect(harness.sentMessages.some((message) => message.customType === "compact-continue")).toBe(
|
|
218
|
+
false
|
|
219
|
+
);
|
|
220
|
+
expect(widgetUpdates.at(-1)).toEqual({ key: "compact-progress", content: undefined });
|
|
221
|
+
expect(workingMessages.at(-1)).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("/clear after deferred fork completion leaves the replacement session idle", async () => {
|
|
225
|
+
const commandDir = mkdtempSync(join(tmpdir(), "clear-fork-command-"));
|
|
226
|
+
const commandPath = join(commandDir, "review.md");
|
|
227
|
+
const deferred = createDeferred<{ duration: number; exitCode: number; output: string }>();
|
|
228
|
+
const workingMessages: string[] = [];
|
|
229
|
+
writeFileSync(commandPath, "Review the code.\n", "utf-8");
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
registerContextForkExtension(harness.api, {
|
|
233
|
+
buildFrontmatterIndex: () =>
|
|
234
|
+
new Map([
|
|
235
|
+
[
|
|
236
|
+
"review",
|
|
237
|
+
{
|
|
238
|
+
context: "fork",
|
|
239
|
+
filePath: commandPath,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
]),
|
|
243
|
+
loadAllAgents: () => new Map(),
|
|
244
|
+
routeForkedModel: async () => undefined,
|
|
245
|
+
spawnForkSubprocess: () => deferred.promise,
|
|
246
|
+
});
|
|
247
|
+
registerClear(harness.api);
|
|
248
|
+
|
|
249
|
+
const clearCommand = harness.commands.get("clear");
|
|
250
|
+
const ctx = buildContext({
|
|
251
|
+
hasUI: true,
|
|
252
|
+
ui: {
|
|
253
|
+
notify: () => {},
|
|
254
|
+
setWorkingMessage: (message?: string) => {
|
|
255
|
+
workingMessages.push(message ?? "");
|
|
256
|
+
},
|
|
257
|
+
} as ExtensionUIContext,
|
|
258
|
+
isIdle: () => true,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!clearCommand?.handler) {
|
|
262
|
+
throw new Error("expected clear command to be registered");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const [forkResult] = await harness.fireEvent("input", { text: "/review" }, ctx);
|
|
266
|
+
expect(forkResult).toEqual({ action: "handled" });
|
|
267
|
+
expect(harness.sentMessages).toHaveLength(1);
|
|
268
|
+
expect(harness.sentMessages[0]?.content).toContain("🔀 /review");
|
|
269
|
+
|
|
270
|
+
const newSession = mock(async () => {
|
|
271
|
+
await harness.fireEvent(
|
|
272
|
+
"session_before_switch",
|
|
273
|
+
{ type: "session_before_switch", reason: "new" },
|
|
274
|
+
ctx
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
await clearCommand.handler("", { ...ctx, newSession } as never);
|
|
278
|
+
deferred.resolve({ duration: 5, exitCode: 0, output: "fork done" });
|
|
279
|
+
await Promise.resolve();
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
281
|
+
|
|
282
|
+
expect(newSession).toHaveBeenCalledTimes(1);
|
|
283
|
+
expect(workingMessages).toContain("🔀 forking: /review");
|
|
284
|
+
expect(workingMessages.at(-1)).toBe("");
|
|
285
|
+
expect(harness.sentMessages).toHaveLength(1);
|
|
286
|
+
expect(harness.sentMessages.some((message) => message.options?.triggerTurn === true)).toBe(
|
|
287
|
+
false
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const diagnostics = getResetDiagnosticsForTests();
|
|
291
|
+
expect(diagnostics.some((event) => event.kind === "deferred_cancelled")).toBe(true);
|
|
292
|
+
expect(
|
|
293
|
+
diagnostics.some(
|
|
294
|
+
(event) =>
|
|
295
|
+
event.kind === "deferred_dropped" &&
|
|
296
|
+
event.source === "context-fork" &&
|
|
297
|
+
event.reason === "session_generation_mismatch"
|
|
298
|
+
)
|
|
299
|
+
).toBe(true);
|
|
300
|
+
} finally {
|
|
301
|
+
deferred.reject(new Error("cleanup"));
|
|
302
|
+
rmSync(commandDir, { force: true, recursive: true });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
38
305
|
});
|
|
@@ -418,7 +418,7 @@ export function registerCommandExpansionExtension(
|
|
|
418
418
|
|
|
419
419
|
// Only process if it looks like a command with arguments
|
|
420
420
|
const split = splitOuterCommand(text);
|
|
421
|
-
if (!split
|
|
421
|
+
if (!split?.args) {
|
|
422
422
|
return { action: "continue" as const };
|
|
423
423
|
}
|
|
424
424
|
|
|
@@ -943,7 +943,11 @@ export default function contextFilesExtension(pi: ExtensionAPI) {
|
|
|
943
943
|
resetSessionState();
|
|
944
944
|
});
|
|
945
945
|
|
|
946
|
-
|
|
946
|
+
(
|
|
947
|
+
pi as unknown as {
|
|
948
|
+
on: (event: string, handler: () => Promise<void>) => void;
|
|
949
|
+
}
|
|
950
|
+
).on("session_switch", async () => {
|
|
947
951
|
resetSessionState();
|
|
948
952
|
});
|
|
949
953
|
|
|
@@ -6,7 +6,7 @@ import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
|
|
|
6
6
|
import { buildFrontmatterIndex } from "../frontmatter-index.js";
|
|
7
7
|
import { registerContextForkExtension } from "../index.js";
|
|
8
8
|
import { resolveModel } from "../model-resolver.js";
|
|
9
|
-
import type { ForkOptions } from "../spawn.js";
|
|
9
|
+
import type { ForkOptions, ForkResult } from "../spawn.js";
|
|
10
10
|
import { buildForkArgs } from "../spawn.js";
|
|
11
11
|
|
|
12
12
|
// ── Model Resolver ──────────────────────────────────────────
|
|
@@ -503,3 +503,96 @@ describe("context-fork lazy resource initialization", () => {
|
|
|
503
503
|
expect(agentLoads).toBe(2);
|
|
504
504
|
});
|
|
505
505
|
});
|
|
506
|
+
|
|
507
|
+
interface Deferred<T> {
|
|
508
|
+
readonly promise: Promise<T>;
|
|
509
|
+
readonly reject: (error?: unknown) => void;
|
|
510
|
+
readonly resolve: (value: T) => void;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Create a deferred promise for controlling async completion timing in tests.
|
|
515
|
+
*
|
|
516
|
+
* @template T
|
|
517
|
+
* @returns Deferred promise controls
|
|
518
|
+
*/
|
|
519
|
+
function createDeferred<T>(): Deferred<T> {
|
|
520
|
+
let reject!: (error?: unknown) => void;
|
|
521
|
+
let resolve!: (value: T) => void;
|
|
522
|
+
const promise = new Promise<T>((innerResolve, innerReject) => {
|
|
523
|
+
resolve = innerResolve;
|
|
524
|
+
reject = innerReject;
|
|
525
|
+
});
|
|
526
|
+
return { promise, reject, resolve };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Build a minimal extension context with a mutable working-message log.
|
|
531
|
+
*
|
|
532
|
+
* @param workingMessages - Collector for working-message updates
|
|
533
|
+
* @returns Minimal extension context for context-fork tests
|
|
534
|
+
*/
|
|
535
|
+
function buildTestContext(workingMessages: string[]): Record<string, unknown> {
|
|
536
|
+
return {
|
|
537
|
+
cwd: process.cwd(),
|
|
538
|
+
hasUI: true,
|
|
539
|
+
isIdle: () => true,
|
|
540
|
+
model: undefined,
|
|
541
|
+
ui: {
|
|
542
|
+
notify: () => {},
|
|
543
|
+
setWorkingMessage: (message?: string) => {
|
|
544
|
+
workingMessages.push(message ?? "");
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
describe("context-fork reset boundaries", () => {
|
|
551
|
+
test("ignores late fork completion after session_before_switch", async () => {
|
|
552
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fork-reset-test-"));
|
|
553
|
+
const commandPath = path.join(tmpDir, "review.md");
|
|
554
|
+
const deferred = createDeferred<ForkResult>();
|
|
555
|
+
const harness = ExtensionHarness.create();
|
|
556
|
+
const workingMessages: string[] = [];
|
|
557
|
+
fs.writeFileSync(commandPath, "Review the code.\n", "utf-8");
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
registerContextForkExtension(harness.api, {
|
|
561
|
+
buildFrontmatterIndex: () =>
|
|
562
|
+
new Map([
|
|
563
|
+
[
|
|
564
|
+
"review",
|
|
565
|
+
{
|
|
566
|
+
context: "fork",
|
|
567
|
+
filePath: commandPath,
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
]),
|
|
571
|
+
loadAllAgents: () => new Map(),
|
|
572
|
+
routeForkedModel: async () => undefined,
|
|
573
|
+
spawnForkSubprocess: () => deferred.promise,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const ctx = buildTestContext(workingMessages);
|
|
577
|
+
const [result] = await harness.fireEvent("input", { text: "/review" }, ctx as never);
|
|
578
|
+
expect(result).toEqual({ action: "handled" });
|
|
579
|
+
expect(harness.sentMessages).toHaveLength(1);
|
|
580
|
+
expect(harness.sentMessages[0]?.content).toContain("🔀 /review");
|
|
581
|
+
|
|
582
|
+
await harness.fireEvent(
|
|
583
|
+
"session_before_switch",
|
|
584
|
+
{ type: "session_before_switch", reason: "new" },
|
|
585
|
+
ctx as never
|
|
586
|
+
);
|
|
587
|
+
deferred.resolve({ duration: 5, exitCode: 0, output: "fork done" });
|
|
588
|
+
await Promise.resolve();
|
|
589
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
590
|
+
|
|
591
|
+
expect(workingMessages).toContain("🔀 forking: /review");
|
|
592
|
+
expect(harness.sentMessages).toHaveLength(1);
|
|
593
|
+
} finally {
|
|
594
|
+
deferred.reject(new Error("cleanup"));
|
|
595
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Runs commands/skills with context: fork frontmatter in isolated pi subprocesses",
|
|
5
5
|
"whenToUse": "Use when a command or skill must run in an isolated forked tallow subprocess.",
|
|
6
6
|
"capabilities": {
|
|
7
|
-
"events": ["input", "session_start"]
|
|
7
|
+
"events": ["input", "session_before_switch", "session_start"]
|
|
8
8
|
},
|
|
9
9
|
"permissionSurface": {
|
|
10
10
|
"filesystem": "write",
|
|
@@ -23,6 +23,7 @@ import * as path from "node:path";
|
|
|
23
23
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
24
24
|
import { stripFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
25
25
|
import { Text } from "@mariozechner/pi-tui";
|
|
26
|
+
import { recordResetDiagnostic } from "../../src/reset-diagnostics.js";
|
|
26
27
|
import { createLazyInitializer } from "../_shared/lazy-init.js";
|
|
27
28
|
import { isProjectTrusted } from "../_shared/project-trust.js";
|
|
28
29
|
import { isShellInterpolationEnabled } from "../_shared/shell-policy.js";
|
|
@@ -326,6 +327,7 @@ export function registerContextForkExtension(
|
|
|
326
327
|
|
|
327
328
|
let frontmatterIndex: FrontmatterIndex = new Map();
|
|
328
329
|
let agents: Map<string, AgentConfig> = new Map();
|
|
330
|
+
let sessionGeneration = 0;
|
|
329
331
|
|
|
330
332
|
const debug = (...args: unknown[]) => {
|
|
331
333
|
if (process.env.DEBUG) {
|
|
@@ -375,9 +377,21 @@ export function registerContextForkExtension(
|
|
|
375
377
|
|
|
376
378
|
// Reset lazy state on each session start so resources reflect on-disk changes.
|
|
377
379
|
pi.on("session_start", async () => {
|
|
380
|
+
sessionGeneration += 1;
|
|
378
381
|
resetResources();
|
|
379
382
|
});
|
|
380
383
|
|
|
384
|
+
// Invalidate any in-flight fork completions before switching sessions.
|
|
385
|
+
pi.on("session_before_switch", async (_event, ctx) => {
|
|
386
|
+
sessionGeneration += 1;
|
|
387
|
+
ctx.ui?.setWorkingMessage?.();
|
|
388
|
+
recordResetDiagnostic({
|
|
389
|
+
kind: "deferred_cancelled",
|
|
390
|
+
reason: "session_before_switch",
|
|
391
|
+
source: "context-fork",
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
381
395
|
// Register custom message renderer for fork results
|
|
382
396
|
pi.registerMessageRenderer<ForkResultDetails>("fork-result", (message, _options, theme) => {
|
|
383
397
|
const details = message.details;
|
|
@@ -505,6 +519,8 @@ export function registerContextForkExtension(
|
|
|
505
519
|
|
|
506
520
|
// Mark as handled — prevent command-prompt/minimal-skill-display from processing
|
|
507
521
|
// We continue the fork asynchronously via the promise below
|
|
522
|
+
const forkGeneration = sessionGeneration;
|
|
523
|
+
recordResetDiagnostic({ kind: "deferred_registered", source: "context-fork" });
|
|
508
524
|
const forkPromise = dependencies.spawnForkSubprocess({
|
|
509
525
|
content,
|
|
510
526
|
cwd: ctx.cwd,
|
|
@@ -516,6 +532,14 @@ export function registerContextForkExtension(
|
|
|
516
532
|
|
|
517
533
|
forkPromise
|
|
518
534
|
.then((result) => {
|
|
535
|
+
if (forkGeneration !== sessionGeneration) {
|
|
536
|
+
recordResetDiagnostic({
|
|
537
|
+
kind: "deferred_dropped",
|
|
538
|
+
reason: "session_generation_mismatch",
|
|
539
|
+
source: "context-fork",
|
|
540
|
+
});
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
519
543
|
ctx.ui.setWorkingMessage();
|
|
520
544
|
|
|
521
545
|
if (result.exitCode !== 0 && !result.output) {
|
|
@@ -550,6 +574,14 @@ export function registerContextForkExtension(
|
|
|
550
574
|
);
|
|
551
575
|
})
|
|
552
576
|
.catch((err: unknown) => {
|
|
577
|
+
if (forkGeneration !== sessionGeneration) {
|
|
578
|
+
recordResetDiagnostic({
|
|
579
|
+
kind: "deferred_dropped",
|
|
580
|
+
reason: "session_generation_mismatch",
|
|
581
|
+
source: "context-fork",
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
553
585
|
ctx.ui.setWorkingMessage();
|
|
554
586
|
const message = err instanceof Error ? err.message : String(err);
|
|
555
587
|
ctx.ui.notify(`Fork /${commandName} error: ${message}`, "error");
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
renderDiff,
|
|
15
15
|
type ThemeColor,
|
|
16
16
|
} from "@mariozechner/pi-coding-agent";
|
|
17
|
-
import {
|
|
17
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
18
18
|
import { getIcon } from "../_icons/index.js";
|
|
19
19
|
import { commandExistsOnPath, runGitCommandSync } from "../_shared/shell-policy.js";
|
|
20
|
+
import { fileLink, hyperlink } from "../_shared/terminal-links.js";
|
|
20
21
|
import {
|
|
21
22
|
appendSection,
|
|
22
23
|
dimProcessOutputLine,
|
|
@@ -1610,11 +1610,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
1610
1610
|
}
|
|
1611
1611
|
});
|
|
1612
1612
|
|
|
1613
|
-
//
|
|
1614
|
-
|
|
1613
|
+
// session_switch/session_fork were removed upstream in favor of session_start
|
|
1614
|
+
// with a reason discriminator. Preserve hook compatibility here.
|
|
1615
|
+
pi.on("session_start", async (event) => {
|
|
1616
|
+
if (event.reason === "resume" || event.reason === "new") {
|
|
1617
|
+
await runHooks("session_switch", {
|
|
1618
|
+
reason: event.reason,
|
|
1619
|
+
previousSessionFile: event.previousSessionFile,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
if (event.reason === "fork") {
|
|
1623
|
+
await runHooks("session_fork", {
|
|
1624
|
+
previousSessionFile: event.previousSessionFile,
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
(
|
|
1629
|
+
pi as unknown as {
|
|
1630
|
+
on: (event: string, handler: (event: Record<string, unknown>) => Promise<void>) => void;
|
|
1631
|
+
}
|
|
1632
|
+
).on("session_switch", async (event) => {
|
|
1615
1633
|
await runHooks("session_switch", {
|
|
1616
|
-
reason: event.reason,
|
|
1617
|
-
previousSessionFile:
|
|
1634
|
+
reason: String(event.reason ?? "resume"),
|
|
1635
|
+
previousSessionFile:
|
|
1636
|
+
typeof event.previousSessionFile === "string" ? event.previousSessionFile : undefined,
|
|
1637
|
+
});
|
|
1638
|
+
});
|
|
1639
|
+
(
|
|
1640
|
+
pi as unknown as {
|
|
1641
|
+
on: (event: string, handler: (event: Record<string, unknown>) => Promise<void>) => void;
|
|
1642
|
+
}
|
|
1643
|
+
).on("session_fork", async (event) => {
|
|
1644
|
+
await runHooks("session_fork", {
|
|
1645
|
+
previousSessionFile:
|
|
1646
|
+
typeof event.previousSessionFile === "string" ? event.previousSessionFile : undefined,
|
|
1618
1647
|
});
|
|
1619
1648
|
});
|
|
1620
1649
|
|
|
@@ -1631,13 +1660,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1631
1660
|
}
|
|
1632
1661
|
});
|
|
1633
1662
|
|
|
1634
|
-
// Hook into session_fork — fires after forking
|
|
1635
|
-
pi.on("session_fork", async (event) => {
|
|
1636
|
-
await runHooks("session_fork", {
|
|
1637
|
-
previousSessionFile: event.previousSessionFile,
|
|
1638
|
-
});
|
|
1639
|
-
});
|
|
1640
|
-
|
|
1641
1663
|
// Hook into session_before_tree — fires before tree navigation (can cancel)
|
|
1642
1664
|
pi.on("session_before_tree", async (event, ctx) => {
|
|
1643
1665
|
const result = await runHooks(
|
package/extensions/loop/index.ts
CHANGED
|
@@ -798,7 +798,20 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
798
798
|
stopLoop(ctx, "Loop stopped (session shutdown)");
|
|
799
799
|
});
|
|
800
800
|
|
|
801
|
-
pi.on("
|
|
801
|
+
pi.on("session_start", async (event, ctx) => {
|
|
802
|
+
if (event.reason !== "startup") {
|
|
803
|
+
stopLoop(ctx, `Loop stopped (session ${event.reason})`);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
(
|
|
808
|
+
pi as unknown as {
|
|
809
|
+
on: (
|
|
810
|
+
event: string,
|
|
811
|
+
handler: (event: unknown, ctx: ExtensionContext) => Promise<void>
|
|
812
|
+
) => void;
|
|
813
|
+
}
|
|
814
|
+
).on("session_switch", async (_event, ctx) => {
|
|
802
815
|
stopLoop(ctx, "Loop stopped (session switch)");
|
|
803
816
|
});
|
|
804
817
|
}
|