@agnishc/edb-subagents 0.10.6 → 0.10.9
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/agent-types.test.ts +126 -0
- package/src/index.ts +2 -2
- package/src/memory.test.ts +40 -0
- package/src/prompts.test.ts +76 -0
- package/src/ui/agent-widget.ts +3 -3
- package/src/usage.test.ts +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.9] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
## [0.10.8] - 2026-05-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Replaced Braille spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏, 10 frames @ 80ms) with Claude Code–style spinner (· ✢ ✳ ✶ ✻ ✽, 6 frames @ 150ms) to match the global footer's working indicator
|
|
9
|
+
|
|
3
10
|
## [0.10.6] - 2026-05-15
|
|
4
11
|
|
|
5
12
|
## [0.10.5] - 2026-05-15
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agnishc/edb-subagents",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.9",
|
|
4
4
|
"description": "Pi extension: Claude Code-style autonomous sub-agents with live widget, parallel execution, mid-run steering, and custom agent types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { getAgentConfig, getAvailableTypes, getToolNamesForType, registerAgents, resolveType } from "./agent-types.js";
|
|
3
|
+
import type { AgentConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("agent-types registry", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Reset registry by re-registering empty map
|
|
8
|
+
registerAgents(new Map());
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("registerAgents adds types to registry", () => {
|
|
12
|
+
const config: AgentConfig = {
|
|
13
|
+
name: "test",
|
|
14
|
+
description: "Test agent",
|
|
15
|
+
model: "test/model",
|
|
16
|
+
extensions: true,
|
|
17
|
+
skills: true,
|
|
18
|
+
promptMode: "append",
|
|
19
|
+
systemPrompt: "Test",
|
|
20
|
+
};
|
|
21
|
+
registerAgents(new Map([["test", config]]));
|
|
22
|
+
expect(getAvailableTypes()).toContain("test");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("resolveType returns config for registered type", () => {
|
|
26
|
+
const config: AgentConfig = {
|
|
27
|
+
name: "coder",
|
|
28
|
+
description: "Coder agent",
|
|
29
|
+
model: "test/model",
|
|
30
|
+
extensions: true,
|
|
31
|
+
skills: true,
|
|
32
|
+
promptMode: "append",
|
|
33
|
+
systemPrompt: "You are a coder",
|
|
34
|
+
};
|
|
35
|
+
registerAgents(new Map([["coder", config]]));
|
|
36
|
+
const resolved = resolveType("coder");
|
|
37
|
+
expect(resolved).toBe("coder");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("resolveType is case-insensitive", () => {
|
|
41
|
+
const config: AgentConfig = {
|
|
42
|
+
name: "Coder",
|
|
43
|
+
description: "Coder",
|
|
44
|
+
model: "m",
|
|
45
|
+
extensions: true,
|
|
46
|
+
skills: true,
|
|
47
|
+
promptMode: "append",
|
|
48
|
+
systemPrompt: "Test",
|
|
49
|
+
};
|
|
50
|
+
registerAgents(new Map([["Coder", config]]));
|
|
51
|
+
expect(resolveType("CODER")).toBe("Coder");
|
|
52
|
+
expect(resolveType("coder")).toBe("Coder");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("resolveType returns undefined for unknown type", () => {
|
|
56
|
+
registerAgents(new Map());
|
|
57
|
+
expect(resolveType("nonexistent")).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("getAgentConfig returns config for registered type", () => {
|
|
61
|
+
const config: AgentConfig = {
|
|
62
|
+
name: "reviewer",
|
|
63
|
+
description: "Reviewer",
|
|
64
|
+
model: "test/model",
|
|
65
|
+
extensions: true,
|
|
66
|
+
skills: true,
|
|
67
|
+
promptMode: "append",
|
|
68
|
+
systemPrompt: "You are a reviewer",
|
|
69
|
+
};
|
|
70
|
+
registerAgents(new Map([["reviewer", config]]));
|
|
71
|
+
const result = getAgentConfig("reviewer");
|
|
72
|
+
expect(result?.name).toBe("reviewer");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("getAgentConfig returns undefined for unknown type", () => {
|
|
76
|
+
registerAgents(new Map());
|
|
77
|
+
expect(getAgentConfig("nonexistent")).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("getAvailableTypes excludes disabled agents", () => {
|
|
81
|
+
const config: AgentConfig = {
|
|
82
|
+
name: "disabled",
|
|
83
|
+
description: "Disabled",
|
|
84
|
+
model: "test/model",
|
|
85
|
+
enabled: false,
|
|
86
|
+
extensions: true,
|
|
87
|
+
skills: true,
|
|
88
|
+
promptMode: "append",
|
|
89
|
+
systemPrompt: "Disabled",
|
|
90
|
+
};
|
|
91
|
+
registerAgents(new Map([["disabled", config]]));
|
|
92
|
+
expect(getAvailableTypes()).not.toContain("disabled");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("getToolNamesForType returns tools for registered type", () => {
|
|
96
|
+
const config: AgentConfig = {
|
|
97
|
+
name: "coder",
|
|
98
|
+
description: "Coder",
|
|
99
|
+
model: "m",
|
|
100
|
+
builtinToolNames: ["read", "write", "bash"],
|
|
101
|
+
extensions: true,
|
|
102
|
+
skills: true,
|
|
103
|
+
promptMode: "append",
|
|
104
|
+
systemPrompt: "Test",
|
|
105
|
+
};
|
|
106
|
+
registerAgents(new Map([["coder", config]]));
|
|
107
|
+
const tools = getToolNamesForType("coder");
|
|
108
|
+
expect(tools).toContain("read");
|
|
109
|
+
expect(tools).toContain("write");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("getToolNamesForType returns default tools when not specified", () => {
|
|
113
|
+
const config: AgentConfig = {
|
|
114
|
+
name: "custom",
|
|
115
|
+
description: "Custom",
|
|
116
|
+
model: "m",
|
|
117
|
+
extensions: true,
|
|
118
|
+
skills: true,
|
|
119
|
+
promptMode: "append",
|
|
120
|
+
systemPrompt: "Test",
|
|
121
|
+
};
|
|
122
|
+
registerAgents(new Map([["custom", config]]));
|
|
123
|
+
const tools = getToolNamesForType("custom");
|
|
124
|
+
expect(tools).toContain("read");
|
|
125
|
+
});
|
|
126
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1168,11 +1168,11 @@ Guidelines:
|
|
|
1168
1168
|
}
|
|
1169
1169
|
};
|
|
1170
1170
|
|
|
1171
|
-
// Animate spinner at
|
|
1171
|
+
// Animate spinner at 150ms (Claude Code–style 6-frame rotation)
|
|
1172
1172
|
const spinnerInterval = setInterval(() => {
|
|
1173
1173
|
spinnerFrame++;
|
|
1174
1174
|
streamUpdate();
|
|
1175
|
-
},
|
|
1175
|
+
}, 150);
|
|
1176
1176
|
|
|
1177
1177
|
streamUpdate();
|
|
1178
1178
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isUnsafeName } from "./memory.js";
|
|
3
|
+
|
|
4
|
+
describe("isUnsafeName", () => {
|
|
5
|
+
it("returns false for safe names", () => {
|
|
6
|
+
expect(isUnsafeName("index")).toBe(false);
|
|
7
|
+
expect(isUnsafeName("index.ts")).toBe(false);
|
|
8
|
+
expect(isUnsafeName("my-agent")).toBe(false);
|
|
9
|
+
expect(isUnsafeName("my_agent_123")).toBe(false);
|
|
10
|
+
expect(isUnsafeName("Agent2")).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns true for empty name", () => {
|
|
14
|
+
expect(isUnsafeName("")).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns true for names over 128 chars", () => {
|
|
18
|
+
expect(isUnsafeName("a".repeat(129))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns true for names starting with dot", () => {
|
|
22
|
+
expect(isUnsafeName(".git")).toBe(true);
|
|
23
|
+
expect(isUnsafeName(".DS_Store")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("allows underscores in safe names", () => {
|
|
27
|
+
// The regex allows underscores, so node_modules is technically "safe"
|
|
28
|
+
expect(isUnsafeName("node_modules")).toBe(false);
|
|
29
|
+
expect(isUnsafeName("my_agent")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns true for names with special characters", () => {
|
|
33
|
+
expect(isUnsafeName("agent@home")).toBe(true);
|
|
34
|
+
expect(isUnsafeName("agent space")).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns true for leading hyphen", () => {
|
|
38
|
+
expect(isUnsafeName("-agent")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildAgentPrompt } from "./prompts.js";
|
|
3
|
+
import type { AgentConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("buildAgentPrompt", () => {
|
|
6
|
+
const baseConfig: AgentConfig = {
|
|
7
|
+
name: "Test Agent",
|
|
8
|
+
description: "Test agent",
|
|
9
|
+
extensions: true,
|
|
10
|
+
skills: true,
|
|
11
|
+
promptMode: "replace",
|
|
12
|
+
systemPrompt: "You are a test agent.",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const baseEnv = {
|
|
16
|
+
cwd: "/tmp/test",
|
|
17
|
+
platform: "darwin",
|
|
18
|
+
isGitRepo: false,
|
|
19
|
+
branch: "",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
it("includes active_agent tag", () => {
|
|
23
|
+
const result = buildAgentPrompt(baseConfig, baseEnv.cwd, baseEnv);
|
|
24
|
+
expect(result).toContain('<active_agent name="Test Agent"/>');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("includes environment info", () => {
|
|
28
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv);
|
|
29
|
+
expect(result).toContain("Working directory: /tmp/test");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("includes platform in replace mode", () => {
|
|
33
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv);
|
|
34
|
+
expect(result).toContain("Platform: darwin");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("includes system prompt in replace mode", () => {
|
|
38
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv);
|
|
39
|
+
expect(result).toContain("You are a test agent.");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("includes git info when in git repo", () => {
|
|
43
|
+
const env = { ...baseEnv, isGitRepo: true, branch: "main" };
|
|
44
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", env);
|
|
45
|
+
expect(result).toContain("Git repository: yes");
|
|
46
|
+
expect(result).toContain("Branch: main");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("includes parent system prompt in append mode", () => {
|
|
50
|
+
const config: AgentConfig = { ...baseConfig, promptMode: "append" };
|
|
51
|
+
const result = buildAgentPrompt(config, "/tmp/test", baseEnv, "You are the parent.");
|
|
52
|
+
expect(result).toContain("You are the parent.");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("includes agent instructions in append mode", () => {
|
|
56
|
+
const config: AgentConfig = { ...baseConfig, promptMode: "append" };
|
|
57
|
+
const result = buildAgentPrompt(config, "/tmp/test", baseEnv);
|
|
58
|
+
expect(result).toContain("<agent_instructions>");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("includes memory block when provided", () => {
|
|
62
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv, undefined, {
|
|
63
|
+
memoryBlock: "# Memory\nPrevious work: done",
|
|
64
|
+
});
|
|
65
|
+
expect(result).toContain("Memory");
|
|
66
|
+
expect(result).toContain("Previous work: done");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes skill blocks when provided", () => {
|
|
70
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv, undefined, {
|
|
71
|
+
skillBlocks: [{ name: "TestSkill", content: "Test content" }],
|
|
72
|
+
});
|
|
73
|
+
expect(result).toContain("Preloaded Skill: TestSkill");
|
|
74
|
+
expect(result).toContain("Test content");
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -16,8 +16,8 @@ import { getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type Se
|
|
|
16
16
|
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
17
17
|
const MAX_WIDGET_LINES = 12;
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
export const SPINNER = ["
|
|
19
|
+
/** Claude Code–style spinner frames for animated running indicator. */
|
|
20
|
+
export const SPINNER = ["·", "✢", "✳", "✶", "✻", "✽"];
|
|
21
21
|
|
|
22
22
|
/** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
|
|
23
23
|
export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
|
|
@@ -248,7 +248,7 @@ export class AgentWidget {
|
|
|
248
248
|
/** Ensure the widget update timer is running. */
|
|
249
249
|
ensureTimer() {
|
|
250
250
|
if (!this.widgetInterval) {
|
|
251
|
-
this.widgetInterval = setInterval(() => this.update(),
|
|
251
|
+
this.widgetInterval = setInterval(() => this.update(), 150);
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { LifetimeUsage } from "./usage.js";
|
|
3
|
+
import { getLifetimeTotal, getSessionContextPercent, getSessionTokens } from "./usage.js";
|
|
4
|
+
|
|
5
|
+
describe("getLifetimeTotal", () => {
|
|
6
|
+
it("returns 0 when undefined", () => {
|
|
7
|
+
expect(getLifetimeTotal(undefined)).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("sums tokens from usage object", () => {
|
|
11
|
+
const usage: LifetimeUsage = { input: 100, output: 200, cacheWrite: 50 };
|
|
12
|
+
expect(getLifetimeTotal(usage)).toBe(350);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("handles single component", () => {
|
|
16
|
+
const usage: LifetimeUsage = { input: 500, output: 0, cacheWrite: 0 };
|
|
17
|
+
expect(getLifetimeTotal(usage)).toBe(500);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("getSessionTokens", () => {
|
|
22
|
+
it("returns 0 for undefined session", () => {
|
|
23
|
+
expect(getSessionTokens(undefined)).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns 0 when getSessionStats throws", () => {
|
|
27
|
+
const badSession = {
|
|
28
|
+
getSessionStats: () => {
|
|
29
|
+
throw new Error("no stats");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
expect(getSessionTokens(badSession as any)).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("sums tokens from session stats", () => {
|
|
36
|
+
const session = {
|
|
37
|
+
getSessionStats: () => ({
|
|
38
|
+
tokens: { input: 100, output: 200, cacheWrite: 50 },
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
expect(getSessionTokens(session as any)).toBe(350);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("getSessionContextPercent", () => {
|
|
46
|
+
it("returns null for undefined session", () => {
|
|
47
|
+
expect(getSessionContextPercent(undefined)).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns null when getSessionStats throws", () => {
|
|
51
|
+
const badSession = {
|
|
52
|
+
getSessionStats: () => {
|
|
53
|
+
throw new Error("no stats");
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
expect(getSessionContextPercent(badSession as any)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns context percent from session stats", () => {
|
|
60
|
+
const session = {
|
|
61
|
+
getSessionStats: () => ({
|
|
62
|
+
tokens: { input: 100, output: 200, cacheWrite: 0 },
|
|
63
|
+
contextUsage: { percent: 75 },
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
expect(getSessionContextPercent(session as any)).toBe(75);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns null when percent is missing", () => {
|
|
70
|
+
const session = {
|
|
71
|
+
getSessionStats: () => ({
|
|
72
|
+
tokens: { input: 100, output: 200, cacheWrite: 0 },
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
expect(getSessionContextPercent(session as any)).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|