@dungle-scrubs/tallow 0.8.26 → 0.8.28
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/README.md +42 -1
- package/dist/cli.js +7 -1
- 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 +1 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +40 -1
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +2 -5
- package/dist/model-metadata-overrides.d.ts.map +1 -1
- package/dist/model-metadata-overrides.js +23 -12
- package/dist/model-metadata-overrides.js.map +1 -1
- package/dist/pid-manager.d.ts +2 -9
- package/dist/pid-manager.d.ts.map +1 -1
- package/dist/pid-manager.js +1 -58
- package/dist/pid-manager.js.map +1 -1
- package/dist/pid-schema.d.ts +51 -0
- package/dist/pid-schema.d.ts.map +1 -0
- package/dist/pid-schema.js +70 -0
- package/dist/pid-schema.js.map +1 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +24 -17
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +53 -3
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/dist/workspace-transition.d.ts +2 -1
- package/dist/workspace-transition.d.ts.map +1 -1
- package/dist/workspace-transition.js +16 -4
- package/dist/workspace-transition.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +309 -0
- package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
- package/extensions/__integration__/tasks-runtime.test.ts +63 -12
- package/extensions/__integration__/welcome-screen.test.ts +240 -0
- package/extensions/_shared/lazy-init.ts +88 -3
- package/extensions/_shared/pid-registry.ts +8 -82
- package/extensions/background-task-tool/index.ts +1 -1
- package/extensions/cd-tool/index.ts +4 -1
- package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
- package/extensions/clear/__tests__/clear.test.ts +38 -0
- package/extensions/edit-tool-enhanced/index.ts +3 -1
- package/extensions/git-status/__tests__/git-status.test.ts +32 -0
- package/extensions/health/__tests__/diagnostics.test.ts +25 -0
- package/extensions/health/index.ts +61 -0
- package/extensions/loop/__tests__/loop.test.ts +365 -1
- package/extensions/loop/index.ts +213 -3
- package/extensions/mcp-adapter-tool/index.ts +1 -1
- package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
- package/extensions/permissions/__tests__/permissions.test.ts +213 -0
- package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
- package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
- package/extensions/prompt-suggestions/autocomplete.ts +23 -5
- package/extensions/prompt-suggestions/index.ts +62 -3
- package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
- package/extensions/read-tool-enhanced/index.ts +5 -1
- package/extensions/session-memory/index.ts +1 -1
- package/extensions/session-namer/index.ts +1 -1
- package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +9 -8
- package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
- package/extensions/subagent-tool/formatting.ts +2 -0
- package/extensions/subagent-tool/index.ts +160 -97
- package/extensions/subagent-tool/process.ts +152 -40
- package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
- package/extensions/tasks/extension.json +1 -0
- package/extensions/tasks/index.ts +2 -12
- package/extensions/tasks/state/index.ts +26 -0
- package/extensions/teams-tool/dashboard.ts +13 -1
- package/extensions/teams-tool/sessions/spawn.ts +2 -2
- package/extensions/teams-tool/tools/register-extension.ts +10 -2
- package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
- package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
- package/extensions/welcome-screen/extension.json +20 -0
- package/extensions/welcome-screen/index.ts +189 -0
- package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
- package/extensions/wezterm-notify/index.ts +5 -3
- package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +9 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +50 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
- package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +64 -1
- package/package.json +11 -10
- package/runtime/config.ts +7 -0
- package/runtime/model-metadata-overrides.ts +7 -0
- package/runtime/pid-schema.ts +13 -0
- package/skills/tallow-expert/SKILL.md +7 -5
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import registerUpstream from "../index.js";
|
|
7
|
+
|
|
8
|
+
describe("upstream-check extension", () => {
|
|
9
|
+
test("does not register command when packages/tallow-tui is absent", () => {
|
|
10
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "upstream-test-"));
|
|
11
|
+
const originalCwd = process.cwd();
|
|
12
|
+
try {
|
|
13
|
+
process.chdir(tmpDir);
|
|
14
|
+
const commands: string[] = [];
|
|
15
|
+
const pi = {
|
|
16
|
+
registerCommand: (name: string) => {
|
|
17
|
+
commands.push(name);
|
|
18
|
+
},
|
|
19
|
+
} as unknown as ExtensionAPI;
|
|
20
|
+
|
|
21
|
+
registerUpstream(pi);
|
|
22
|
+
expect(commands).not.toContain("upstream");
|
|
23
|
+
} finally {
|
|
24
|
+
process.chdir(originalCwd);
|
|
25
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("registers upstream command when packages/tallow-tui exists", () => {
|
|
30
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "upstream-test-"));
|
|
31
|
+
mkdirSync(join(tmpDir, "packages", "tallow-tui"), { recursive: true });
|
|
32
|
+
const originalCwd = process.cwd();
|
|
33
|
+
try {
|
|
34
|
+
process.chdir(tmpDir);
|
|
35
|
+
const commands: string[] = [];
|
|
36
|
+
const pi = {
|
|
37
|
+
registerCommand: (name: string) => {
|
|
38
|
+
commands.push(name);
|
|
39
|
+
},
|
|
40
|
+
} as unknown as ExtensionAPI;
|
|
41
|
+
|
|
42
|
+
registerUpstream(pi);
|
|
43
|
+
expect(commands).toContain("upstream");
|
|
44
|
+
} finally {
|
|
45
|
+
process.chdir(originalCwd);
|
|
46
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import welcomeScreen from "../index.js";
|
|
4
|
+
|
|
5
|
+
describe("welcome-screen extension", () => {
|
|
6
|
+
test("registers session_start handler", () => {
|
|
7
|
+
const events: string[] = [];
|
|
8
|
+
const pi = {
|
|
9
|
+
on: (event: string) => {
|
|
10
|
+
events.push(event);
|
|
11
|
+
},
|
|
12
|
+
} as unknown as ExtensionAPI;
|
|
13
|
+
|
|
14
|
+
welcomeScreen(pi);
|
|
15
|
+
expect(events).toContain("session_start");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("does not register any commands or tools", () => {
|
|
19
|
+
const commands: string[] = [];
|
|
20
|
+
const tools: string[] = [];
|
|
21
|
+
const pi = {
|
|
22
|
+
on: () => {},
|
|
23
|
+
registerCommand: (name: string) => {
|
|
24
|
+
commands.push(name);
|
|
25
|
+
},
|
|
26
|
+
registerTool: (opts: { name: string }) => {
|
|
27
|
+
tools.push(opts.name);
|
|
28
|
+
},
|
|
29
|
+
} as unknown as ExtensionAPI;
|
|
30
|
+
|
|
31
|
+
welcomeScreen(pi);
|
|
32
|
+
expect(commands).toHaveLength(0);
|
|
33
|
+
expect(tools).toHaveLength(0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "welcome-screen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ASCII art welcome screen with version display and update notification",
|
|
5
|
+
"whenToUse": "Replaces the default startup header with a branded ASCII art welcome screen.",
|
|
6
|
+
"capabilities": {
|
|
7
|
+
"events": ["session_start"]
|
|
8
|
+
},
|
|
9
|
+
"permissionSurface": {
|
|
10
|
+
"filesystem": "none",
|
|
11
|
+
"shell": false,
|
|
12
|
+
"network": true,
|
|
13
|
+
"subprocess": false
|
|
14
|
+
},
|
|
15
|
+
"category": "ui",
|
|
16
|
+
"tags": ["welcome", "header", "startup", "branding"],
|
|
17
|
+
"files": ["index.ts"],
|
|
18
|
+
"relationships": [],
|
|
19
|
+
"npmDependencies": {}
|
|
20
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Welcome Screen Extension
|
|
3
|
+
*
|
|
4
|
+
* Replaces the default pi framework startup header with a branded ASCII art
|
|
5
|
+
* welcome screen showing the tallow logo, version, and update availability.
|
|
6
|
+
*
|
|
7
|
+
* The ASCII art is the tallow "T_" mark — a blocky amber T with a cursor,
|
|
8
|
+
* evoking the retro CRT terminal aesthetic of the logo.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
13
|
+
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
14
|
+
import { TALLOW_VERSION } from "../../runtime/config.js";
|
|
15
|
+
|
|
16
|
+
/** Timeout for npm registry fetch (ms). */
|
|
17
|
+
const FETCH_TIMEOUT = 4_000;
|
|
18
|
+
|
|
19
|
+
/** Registry URL for the tallow package. */
|
|
20
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@dungle-scrubs/tallow/latest";
|
|
21
|
+
|
|
22
|
+
// ─── ASCII Art ───────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The tallow "T_" mark — a blocky T with a cursor block.
|
|
26
|
+
* Proportions mirror the logo: wide top bar, centered thick stem, cursor lower-right.
|
|
27
|
+
*/
|
|
28
|
+
const LOGO_LINES = [" ▐████████████▌ ", " ████ ", " ████ ▐█▌ "];
|
|
29
|
+
|
|
30
|
+
// ─── Version Check ───────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetch the latest published version from the npm registry.
|
|
34
|
+
*
|
|
35
|
+
* @returns Latest version string, or null on failure
|
|
36
|
+
*/
|
|
37
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
38
|
+
if (process.env.PI_SKIP_VERSION_CHECK === "1" || process.env.PI_OFFLINE) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(REGISTRY_URL, {
|
|
43
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) return null;
|
|
46
|
+
const data = (await res.json()) as { version?: string };
|
|
47
|
+
return data.version ?? null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compare two semver strings. Returns true if `latest` is newer than `current`.
|
|
55
|
+
*
|
|
56
|
+
* @param current - Currently installed version
|
|
57
|
+
* @param latest - Latest available version
|
|
58
|
+
* @returns True if latest is a newer version
|
|
59
|
+
*/
|
|
60
|
+
function isNewerVersion(current: string, latest: string): boolean {
|
|
61
|
+
const parse = (v: string): number[] => v.split(".").map(Number);
|
|
62
|
+
const c = parse(current);
|
|
63
|
+
const l = parse(latest);
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
|
|
66
|
+
if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Colors ──────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/** Amber/gold (matches the logo glow). */
|
|
74
|
+
const AMBER = "\x1b[38;2;255;191;0m";
|
|
75
|
+
/** Dim amber for version text. */
|
|
76
|
+
const DIM_AMBER = "\x1b[38;2;180;130;30m";
|
|
77
|
+
/** Green for update notification. */
|
|
78
|
+
const GREEN = "\x1b[38;2;100;220;100m";
|
|
79
|
+
/** Reset all styles. */
|
|
80
|
+
const RESET = "\x1b[0m";
|
|
81
|
+
|
|
82
|
+
// ─── Rendering ───────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Center a styled string within a given terminal width.
|
|
86
|
+
*
|
|
87
|
+
* @param line - Styled line (may contain ANSI escapes)
|
|
88
|
+
* @param width - Terminal width
|
|
89
|
+
* @returns Left-padded line
|
|
90
|
+
*/
|
|
91
|
+
function centerLine(line: string, width: number): string {
|
|
92
|
+
const lineWidth = visibleWidth(line);
|
|
93
|
+
const left = Math.max(0, Math.floor((width - lineWidth) / 2));
|
|
94
|
+
return " ".repeat(left) + line;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the welcome screen lines.
|
|
99
|
+
*
|
|
100
|
+
* @param width - Terminal width for centering
|
|
101
|
+
* @param updateVersion - Newer version available, or null
|
|
102
|
+
* @returns Array of styled terminal lines
|
|
103
|
+
*/
|
|
104
|
+
function buildWelcomeLines(width: number, updateVersion: string | null): string[] {
|
|
105
|
+
const lines: string[] = [];
|
|
106
|
+
|
|
107
|
+
// Logo with amber coloring
|
|
108
|
+
for (const logoLine of LOGO_LINES) {
|
|
109
|
+
lines.push(centerLine(`${AMBER}${logoLine}${RESET}`, width));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Version line — dim amber, centered below logo
|
|
113
|
+
lines.push(centerLine(`${DIM_AMBER}tallow v${TALLOW_VERSION}${RESET}`, width));
|
|
114
|
+
|
|
115
|
+
// Update notification
|
|
116
|
+
if (updateVersion) {
|
|
117
|
+
lines.push(centerLine(`${GREEN}update available: v${updateVersion}${RESET}`, width));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Extension Entry ─────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Welcome screen extension.
|
|
127
|
+
* Replaces the default header with an ASCII art logo on session_start.
|
|
128
|
+
*
|
|
129
|
+
* @param pi - Extension API
|
|
130
|
+
*/
|
|
131
|
+
export default function welcomeScreenExtension(pi: ExtensionAPI): void {
|
|
132
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
133
|
+
// Skip for resumed/continued sessions — only show on fresh instances.
|
|
134
|
+
// Filter to message entries only — metadata entries like model_change and
|
|
135
|
+
// thinking_level_change are injected during session setup and exist even
|
|
136
|
+
// on a brand-new session. The role lives on entry.message, not the entry itself.
|
|
137
|
+
const hasConversation = ctx.sessionManager.getEntries().some((e) => {
|
|
138
|
+
const msg = (e as unknown as Record<string, unknown>).message as
|
|
139
|
+
| { role?: string }
|
|
140
|
+
| undefined;
|
|
141
|
+
return msg?.role === "user" || msg?.role === "assistant";
|
|
142
|
+
});
|
|
143
|
+
if (hasConversation) return;
|
|
144
|
+
|
|
145
|
+
let updateVersion: string | null = null;
|
|
146
|
+
let tuiRef: TUI | null = null;
|
|
147
|
+
|
|
148
|
+
// Set the header immediately with current version (no update info yet)
|
|
149
|
+
ctx.ui.setHeader((tui, _theme) => {
|
|
150
|
+
tuiRef = tui;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
render(width: number): string[] {
|
|
154
|
+
return buildWelcomeLines(width, updateVersion);
|
|
155
|
+
},
|
|
156
|
+
invalidate(): void {
|
|
157
|
+
// No cached state to clear
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Clear the changelog "What's New" children from the header container.
|
|
163
|
+
// setHeader only replaces the builtInHeader text component — the changelog
|
|
164
|
+
// section (DynamicBorder, "What's New" heading, Markdown body) lives as
|
|
165
|
+
// separate children in headerContainer and persists unless removed.
|
|
166
|
+
// headerContainer is the first child of the root TUI component.
|
|
167
|
+
queueMicrotask(() => {
|
|
168
|
+
if (!tuiRef) return;
|
|
169
|
+
const tuiChildren = (tuiRef as unknown as Record<string, unknown>).children as
|
|
170
|
+
| Array<Record<string, unknown>>
|
|
171
|
+
| undefined;
|
|
172
|
+
// headerContainer is the first child of the TUI root
|
|
173
|
+
const headerContainer = tuiChildren?.[0]?.children as { length: number } | undefined;
|
|
174
|
+
if (headerContainer && headerContainer.length > 2) {
|
|
175
|
+
// Keep [0]=Spacer and [1]=custom header, drop the rest (bottom spacer + changelog)
|
|
176
|
+
headerContainer.length = 2;
|
|
177
|
+
tuiRef.requestRender();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Fire-and-forget version check — re-renders header when resolved
|
|
182
|
+
fetchLatestVersion().then((latest) => {
|
|
183
|
+
if (latest && isNewerVersion(TALLOW_VERSION, latest)) {
|
|
184
|
+
updateVersion = latest;
|
|
185
|
+
tuiRef?.requestRender();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -157,13 +157,44 @@ describe("wezterm-notify lifecycle", () => {
|
|
|
157
157
|
const inputResult = rig.lifecycle.onInput();
|
|
158
158
|
|
|
159
159
|
expect(inputResult).toEqual({ action: "continue" });
|
|
160
|
-
|
|
160
|
+
// agent_end is a no-op; input transitions working → done
|
|
161
|
+
expect(getStatusWrites(rig)).toEqual(["", "working", "done"]);
|
|
161
162
|
expect(getHeartbeatWrites(rig)).toEqual(["0"]);
|
|
162
163
|
expect(rig.heartbeatStartCount).toBe(1);
|
|
163
164
|
expect(rig.heartbeatStopCount).toBe(1);
|
|
164
165
|
expect(rig.activeHeartbeatCount).toBe(0);
|
|
165
166
|
});
|
|
166
167
|
|
|
168
|
+
it("stays working through tool execution between agent_end and next agent_start", () => {
|
|
169
|
+
const rig = createLifecycleRig();
|
|
170
|
+
|
|
171
|
+
rig.lifecycle.onSessionStart();
|
|
172
|
+
rig.lifecycle.onBeforeAgentStart();
|
|
173
|
+
rig.lifecycle.onAgentStart();
|
|
174
|
+
// Model returns a tool call — agent_end fires but tool is still executing
|
|
175
|
+
rig.lifecycle.onAgentEnd();
|
|
176
|
+
rig.tickHeartbeat();
|
|
177
|
+
rig.tickHeartbeat();
|
|
178
|
+
|
|
179
|
+
// Status should still be "working" — heartbeat should still be active
|
|
180
|
+
expect(getStatusWrites(rig)).toEqual(["", "working"]);
|
|
181
|
+
expect(rig.activeHeartbeatCount).toBe(1);
|
|
182
|
+
|
|
183
|
+
// Tool finishes, next turn starts
|
|
184
|
+
rig.lifecycle.onBeforeAgentStart();
|
|
185
|
+
rig.lifecycle.onAgentStart();
|
|
186
|
+
rig.lifecycle.onAgentEnd();
|
|
187
|
+
|
|
188
|
+
// Still working — no flicker to "done" between turns
|
|
189
|
+
expect(getStatusWrites(rig)).toEqual(["", "working"]);
|
|
190
|
+
expect(rig.activeHeartbeatCount).toBe(1);
|
|
191
|
+
|
|
192
|
+
// Final input prompt appears
|
|
193
|
+
rig.lifecycle.onInput();
|
|
194
|
+
expect(getStatusWrites(rig)).toEqual(["", "working", "done"]);
|
|
195
|
+
expect(rig.activeHeartbeatCount).toBe(0);
|
|
196
|
+
});
|
|
197
|
+
|
|
167
198
|
it("coalesces duplicate starts without clear flicker", () => {
|
|
168
199
|
const rig = createLifecycleRig();
|
|
169
200
|
|
|
@@ -173,26 +204,30 @@ describe("wezterm-notify lifecycle", () => {
|
|
|
173
204
|
rig.lifecycle.onAgentStart();
|
|
174
205
|
rig.tickHeartbeat();
|
|
175
206
|
rig.tickHeartbeat();
|
|
207
|
+
// agent_end is a no-op — heartbeat keeps running
|
|
176
208
|
rig.lifecycle.onAgentEnd();
|
|
177
209
|
|
|
178
|
-
expect(getStatusWrites(rig)).toEqual(["", "working"
|
|
210
|
+
expect(getStatusWrites(rig)).toEqual(["", "working"]);
|
|
179
211
|
expect(getHeartbeatWrites(rig)).toEqual(["0", "1", "2"]);
|
|
180
212
|
expect(rig.heartbeatStartCount).toBe(1);
|
|
181
|
-
|
|
182
|
-
expect(rig.
|
|
213
|
+
// Heartbeat still active (only input/shutdown stops it)
|
|
214
|
+
expect(rig.heartbeatStopCount).toBe(0);
|
|
215
|
+
expect(rig.activeHeartbeatCount).toBe(1);
|
|
183
216
|
});
|
|
184
217
|
|
|
185
218
|
it("does not permanently clear when input arrives before start", () => {
|
|
186
219
|
const rig = createLifecycleRig();
|
|
187
220
|
|
|
188
221
|
rig.lifecycle.onSessionStart();
|
|
222
|
+
// Input arrives while not working — no-op (already idle)
|
|
189
223
|
const inputResult = rig.lifecycle.onInput();
|
|
190
224
|
rig.lifecycle.onBeforeAgentStart();
|
|
191
225
|
rig.lifecycle.onAgentStart();
|
|
192
226
|
rig.lifecycle.onAgentEnd();
|
|
193
227
|
|
|
194
228
|
expect(inputResult).toEqual({ action: "continue" });
|
|
195
|
-
|
|
229
|
+
// agent_end is a no-op, so status stays "working"
|
|
230
|
+
expect(getStatusWrites(rig)).toEqual(["", "working"]);
|
|
196
231
|
});
|
|
197
232
|
|
|
198
233
|
it("starts heartbeat once per interval and leaves no orphan timers", () => {
|
|
@@ -205,18 +240,21 @@ describe("wezterm-notify lifecycle", () => {
|
|
|
205
240
|
expect(rig.heartbeatStartCount).toBe(1);
|
|
206
241
|
expect(rig.activeHeartbeatCount).toBe(1);
|
|
207
242
|
|
|
243
|
+
// agent_end no longer stops heartbeat — stays active through tool execution
|
|
208
244
|
rig.lifecycle.onAgentEnd();
|
|
245
|
+
expect(rig.heartbeatStopCount).toBe(0);
|
|
246
|
+
expect(rig.activeHeartbeatCount).toBe(1);
|
|
247
|
+
|
|
248
|
+
// input stops the heartbeat
|
|
249
|
+
rig.lifecycle.onInput();
|
|
209
250
|
expect(rig.heartbeatStopCount).toBe(1);
|
|
210
251
|
expect(rig.activeHeartbeatCount).toBe(0);
|
|
211
252
|
|
|
212
|
-
const
|
|
253
|
+
const heartbeatAfterInput = getHeartbeatWrites(rig).length;
|
|
213
254
|
rig.tickHeartbeat();
|
|
214
|
-
expect(getHeartbeatWrites(rig)).toHaveLength(
|
|
215
|
-
|
|
216
|
-
rig.lifecycle.onSessionShutdown();
|
|
217
|
-
expect(rig.heartbeatStopCount).toBe(1);
|
|
218
|
-
expect(rig.activeHeartbeatCount).toBe(0);
|
|
255
|
+
expect(getHeartbeatWrites(rig)).toHaveLength(heartbeatAfterInput);
|
|
219
256
|
|
|
257
|
+
// Session shutdown also stops heartbeat cleanly
|
|
220
258
|
rig.lifecycle.onBeforeAgentStart();
|
|
221
259
|
expect(rig.heartbeatStartCount).toBe(2);
|
|
222
260
|
expect(rig.activeHeartbeatCount).toBe(1);
|
|
@@ -175,11 +175,13 @@ export function createWeztermNotifyLifecycle(
|
|
|
175
175
|
enterWorking();
|
|
176
176
|
},
|
|
177
177
|
onAgentEnd: () => {
|
|
178
|
-
|
|
178
|
+
// Intentional no-op: tools may still be executing after the model
|
|
179
|
+
// finishes generating (e.g. subagent parallel runs). Stay "working"
|
|
180
|
+
// until the input prompt appears, which signals the turn is truly done.
|
|
179
181
|
},
|
|
180
182
|
onInput: () => {
|
|
181
|
-
if (
|
|
182
|
-
leaveWorking("");
|
|
183
|
+
if (isWorking) {
|
|
184
|
+
leaveWorking("done");
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
return { action: "continue" as const };
|