@preapexis/piexis 0.1.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/.github/workflows/publish.yml +35 -0
  2. package/AGENTS.md +21 -41
  3. package/README.md +75 -142
  4. package/extensions/agents.ts +205 -0
  5. package/extensions/ask-question.ts +54 -95
  6. package/extensions/recommended-tools.ts +62 -0
  7. package/extensions/ui.ts +204 -58
  8. package/lib/agent-config.ts +55 -0
  9. package/lib/agents.ts +208 -0
  10. package/lib/question-model.ts +98 -0
  11. package/lib/question-ui.ts +214 -0
  12. package/lib/recommended-tools.ts +119 -0
  13. package/lib/subagent-runner.ts +146 -0
  14. package/lib/ui-tips.json +41 -0
  15. package/package.json +9 -12
  16. package/prompts/init.md +10 -2
  17. package/prompts/summary.md +7 -6
  18. package/schemas/agent.schema.json +48 -0
  19. package/scripts/release.mjs +32 -0
  20. package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
  21. package/skills/caveman/LICENSE-MIT +21 -0
  22. package/skills/caveman/NOTICE +10 -0
  23. package/skills/caveman/SKILL.md +89 -0
  24. package/skills/find-skills/SKILL.md +141 -0
  25. package/skills/ponytail/LICENSE +21 -0
  26. package/skills/ponytail/NOTICE +9 -0
  27. package/skills/ponytail/SKILL.md +52 -0
  28. package/skills-lock.json +6 -0
  29. package/tests/agents.test.mjs +605 -0
  30. package/tests/ask-question-pty.test.mjs +16 -0
  31. package/tests/ask-question.test.mjs +269 -0
  32. package/tests/fixtures/agents-provider.ts +55 -0
  33. package/tests/fixtures/ask-question-pty.py +91 -0
  34. package/tests/fixtures/ask-question-terminal.mjs +69 -0
  35. package/tests/fixtures/recommended-tools-terminal.mjs +103 -0
  36. package/tests/init-workflow.test.mjs +31 -0
  37. package/tests/recommended-tools-pty.test.mjs +16 -0
  38. package/tests/recommended-tools.test.mjs +423 -0
  39. package/tests/resources.test.mjs +66 -0
  40. package/tests/skills-routing.test.mjs +7 -4
  41. package/tests/ui-tip-reload.test.mjs +70 -0
  42. package/tests/ui.test.mjs +450 -0
  43. package/tsconfig.json +1 -0
  44. package/.github/workflows/npm-publish.yml +0 -63
  45. package/PERMISSION_MODES_PLAN.md +0 -279
  46. package/WHAT_YOU_GET.md +0 -153
  47. package/eval/README.md +0 -72
  48. package/eval/permission-cases.json +0 -28
  49. package/eval/permissions.mjs +0 -103
  50. package/extensions/background-tasks.ts +0 -433
  51. package/extensions/background-worker.ts +0 -50
  52. package/extensions/glob.ts +0 -32
  53. package/extensions/mode.ts +0 -397
  54. package/lib/bash-approvals.ts +0 -113
  55. package/lib/bash-policy.ts +0 -164
  56. package/lib/mode-policy.ts +0 -174
  57. package/lib/path-policy.ts +0 -182
  58. package/lib/permission-classifier.ts +0 -128
  59. package/lib/permission-config.ts +0 -91
  60. package/lib/permission-denials.ts +0 -32
  61. package/lib/permission-intent.ts +0 -128
  62. package/lib/permission-taxonomy.ts +0 -15
  63. package/lib/task-broker.ts +0 -65
  64. package/lib/task-checkout.ts +0 -39
  65. package/lib/task-git.ts +0 -44
  66. package/lib/task-patch.ts +0 -106
  67. package/lib/task-permissions.ts +0 -51
  68. package/lib/task-report.ts +0 -62
  69. package/lib/task-worktree-policy.ts +0 -56
  70. package/skills/claude-api/SKILL.md +0 -572
  71. package/skills/claude-api/csharp/claude-api/README.md +0 -362
  72. package/skills/claude-api/csharp/claude-api/batches.md +0 -14
  73. package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
  74. package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
  75. package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
  76. package/skills/claude-api/curl/examples.md +0 -256
  77. package/skills/claude-api/curl/managed-agents.md +0 -371
  78. package/skills/claude-api/go/claude-api/README.md +0 -188
  79. package/skills/claude-api/go/claude-api/files-api.md +0 -23
  80. package/skills/claude-api/go/claude-api/streaming.md +0 -43
  81. package/skills/claude-api/go/claude-api/tool-use.md +0 -220
  82. package/skills/claude-api/go/managed-agents/README.md +0 -564
  83. package/skills/claude-api/java/claude-api/README.md +0 -238
  84. package/skills/claude-api/java/claude-api/files-api.md +0 -27
  85. package/skills/claude-api/java/claude-api/streaming.md +0 -24
  86. package/skills/claude-api/java/claude-api/tool-use.md +0 -228
  87. package/skills/claude-api/java/managed-agents/README.md +0 -443
  88. package/skills/claude-api/php/claude-api/README.md +0 -174
  89. package/skills/claude-api/php/claude-api/batches.md +0 -15
  90. package/skills/claude-api/php/claude-api/files-api.md +0 -13
  91. package/skills/claude-api/php/claude-api/streaming.md +0 -27
  92. package/skills/claude-api/php/claude-api/tool-use.md +0 -253
  93. package/skills/claude-api/php/managed-agents/README.md +0 -443
  94. package/skills/claude-api/python/claude-api/README.md +0 -565
  95. package/skills/claude-api/python/claude-api/batches.md +0 -198
  96. package/skills/claude-api/python/claude-api/files-api.md +0 -170
  97. package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
  98. package/skills/claude-api/python/claude-api/streaming.md +0 -179
  99. package/skills/claude-api/python/claude-api/tool-use.md +0 -627
  100. package/skills/claude-api/python/managed-agents/README.md +0 -335
  101. package/skills/claude-api/ruby/claude-api/README.md +0 -134
  102. package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
  103. package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
  104. package/skills/claude-api/ruby/managed-agents/README.md +0 -394
  105. package/skills/claude-api/shared/admin-api.md +0 -179
  106. package/skills/claude-api/shared/agent-design.md +0 -101
  107. package/skills/claude-api/shared/anthropic-cli.md +0 -269
  108. package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
  109. package/skills/claude-api/shared/cost-optimization.md +0 -233
  110. package/skills/claude-api/shared/error-codes.md +0 -262
  111. package/skills/claude-api/shared/live-sources.md +0 -166
  112. package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
  113. package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
  114. package/skills/claude-api/shared/managed-agents-core.md +0 -359
  115. package/skills/claude-api/shared/managed-agents-environments.md +0 -226
  116. package/skills/claude-api/shared/managed-agents-events.md +0 -290
  117. package/skills/claude-api/shared/managed-agents-memory.md +0 -201
  118. package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
  119. package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
  120. package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
  121. package/skills/claude-api/shared/managed-agents-overview.md +0 -77
  122. package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
  123. package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
  124. package/skills/claude-api/shared/managed-agents-tools.md +0 -504
  125. package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
  126. package/skills/claude-api/shared/model-migration.md +0 -1872
  127. package/skills/claude-api/shared/models.md +0 -142
  128. package/skills/claude-api/shared/platform-availability.md +0 -53
  129. package/skills/claude-api/shared/prompt-audit.md +0 -223
  130. package/skills/claude-api/shared/prompt-caching.md +0 -292
  131. package/skills/claude-api/shared/token-counting.md +0 -56
  132. package/skills/claude-api/shared/tool-use-concepts.md +0 -526
  133. package/skills/claude-api/typescript/claude-api/README.md +0 -405
  134. package/skills/claude-api/typescript/claude-api/batches.md +0 -106
  135. package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
  136. package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
  137. package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
  138. package/skills/claude-api/typescript/managed-agents/README.md +0 -359
  139. package/tests/auto-permissions.test.mjs +0 -315
  140. package/tests/background-tasks.test.mjs +0 -27
  141. package/tests/background-worker.test.mjs +0 -60
  142. package/tests/bash-permissions.test.mjs +0 -276
  143. package/tests/glob.test.mjs +0 -31
  144. package/tests/mode.test.mjs +0 -62
  145. package/tests/permission-evaluation.test.mjs +0 -70
  146. package/tests/permission-foundation.test.mjs +0 -194
  147. package/tests/permission-policy.test.mjs +0 -158
  148. package/tests/runtime/README.md +0 -51
  149. package/tests/runtime/harness.mjs +0 -190
  150. package/tests/runtime/permissions.test.mjs +0 -192
  151. package/tests/runtime/tui.test.mjs +0 -60
  152. package/tests/task-integration.test.mjs +0 -324
  153. package/tests/task-permissions.test.mjs +0 -178
  154. package/tests/task-worktree-policy.test.mjs +0 -53
package/extensions/ui.ts CHANGED
@@ -1,20 +1,108 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { truncateToWidth } from "@earendil-works/pi-tui";
3
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
4
4
 
5
+ import { readFileSync } from "node:fs";
5
6
  import { basename } from "node:path";
7
+ import { agentConfigCapabilities } from "../lib/agent-config.ts";
8
+ import { hasProjectAgentConfig, loadAgentConfig } from "../lib/agents.ts";
9
+
10
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
11
+
12
+ const WORDMARK = [
13
+ "██████╗ ██╗███████╗██╗ ██╗██╗███████╗",
14
+ "██╔══██╗██║██╔════╝╚██╗██╔╝██║██╔════╝",
15
+ "██████╔╝██║█████╗ ╚███╔╝ ██║███████╗",
16
+ "██╔═══╝ ██║██╔══╝ ██╔██╗ ██║╚════██║",
17
+ "██║ ██║███████╗██╔╝ ██╗██║███████║",
18
+ "╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝╚══════╝"
19
+ ];
20
+ const WORDMARK_WIDTH = Math.max(...WORDMARK.map((line) => visibleWidth(line)));
21
+
22
+ const fmt = (n: number) => {
23
+ if (n < 1_000) return `${n}`;
24
+ if (n < 1_000_000) return `${(n / 1_000).toFixed(1)}k`;
25
+ return `${(n / 1_000_000).toFixed(1)}m`;
26
+ };
27
+
28
+ type Seg = { text: string; priority: number };
29
+
30
+ // Drop optional fields before shortening the last (highest-priority) field.
31
+ function justify(left: string, right: Seg[], sep: string, width: number): string {
32
+ const segs = [...right];
33
+ while (segs.length) {
34
+ const r = segs.map((s) => s.text).join(sep);
35
+ const gap = width - visibleWidth(left) - visibleWidth(r);
36
+ if (gap >= 2) return left + " ".repeat(gap) + r;
37
+ let weakest = 0;
38
+ segs.forEach((s, i) => {
39
+ if (s.priority < segs[weakest].priority) weakest = i;
40
+ });
41
+ if (segs.length === 1) {
42
+ const available = width - visibleWidth(left) - 2;
43
+ if (available > 0) {
44
+ const text = truncateToWidth(r, available, "…");
45
+ return left + " ".repeat(width - visibleWidth(left) - visibleWidth(text)) + text;
46
+ }
47
+ break;
48
+ }
49
+ segs.splice(weakest, 1);
50
+ }
51
+ return truncateToWidth(left, width, "…");
52
+ }
6
53
 
7
54
  export default function (pi: ExtensionAPI) {
55
+ // Jiti can retain imported JSON across reloads; read it once per extension runtime.
56
+ const tipList: { category: string; text: string }[] = JSON.parse(
57
+ readFileSync(new URL("../lib/ui-tips.json", import.meta.url), "utf8")
58
+ );
59
+ if (!Array.isArray(tipList) || tipList.some((tip) =>
60
+ typeof tip?.category !== "string" || !tip.category.trim() ||
61
+ typeof tip?.text !== "string" || !tip.text.trim()
62
+ )) {
63
+ throw new Error("Invalid ui-tips.json: expected tips with non-empty category and text strings.");
64
+ }
65
+
8
66
  let state = "ready";
9
67
  let activeTool = "";
10
68
  let redraw = () => {};
69
+ let remainingTips: typeof tipList = [];
70
+ let agentSetupTips: string[] = [];
11
71
 
12
72
  function update() {
13
73
  redraw();
14
74
  }
15
75
 
16
76
  pi.on("session_start", async (_event, ctx) => {
77
+ state = "ready";
78
+ activeTool = "";
79
+ redraw = () => {};
80
+ if (ctx.mode !== "tui") return;
81
+
17
82
  const project = basename(ctx.cwd);
83
+ agentSetupTips = [];
84
+ try {
85
+ if (ctx.isProjectTrusted() && await hasProjectAgentConfig(ctx.cwd)) {
86
+ const config = await loadAgentConfig(ctx.cwd, getAgentDir(), true);
87
+ for (const key of Object.keys(agentConfigCapabilities) as (keyof typeof agentConfigCapabilities)[]) {
88
+ if (config[key] === undefined) agentSetupTips.push(`Agent config: add ${key} to ${agentConfigCapabilities[key]}.`);
89
+ }
90
+ }
91
+ } catch {
92
+ agentSetupTips = ["Agent config: fix agent.json so PieXis can apply your agent settings."];
93
+ }
94
+ const selectedTips: typeof tipList = [];
95
+ for (let i = 0; i < Math.min(3, tipList.length); i++) {
96
+ if (remainingTips.length === 0) {
97
+ remainingTips = tipList.filter((tip) => !selectedTips.includes(tip));
98
+ }
99
+ const differentCategories = remainingTips.filter(
100
+ (tip) => !selectedTips.some((selected) => selected.category === tip.category)
101
+ );
102
+ const candidates = differentCategories.length > 0 ? differentCategories : remainingTips;
103
+ const tip = candidates[Math.floor(Math.random() * candidates.length)];
104
+ selectedTips.push(remainingTips.splice(remainingTips.indexOf(tip), 1)[0]);
105
+ }
18
106
 
19
107
  // Put project info in the terminal tab instead of wasting TUI rows.
20
108
  ctx.ui.setTitle(`π ${project}`);
@@ -23,82 +111,126 @@ export default function (pi: ExtensionAPI) {
23
111
  // stay attached to the editor forever, making the onboarding copy noisy.
24
112
  ctx.ui.setHeader((_tui, theme) => ({
25
113
  render(width: number) {
26
- const rule = theme.fg(
27
- "dim",
28
- "─".repeat(Math.max(0, Math.min(width, 72)))
29
- );
30
- const lines =
31
- width < 72
32
- ? [
33
- `${theme.fg("accent", "✦")} ${theme.bold("PieXis")} ${theme.fg("dim", project)}`,
34
- `${theme.fg("muted", "Try:")} explain code · find bugs · review diffs · run tests`,
35
- `${theme.fg("dim", "/plan <task> · Alt+M modes · /help for commands")}`
36
- ]
37
- : [
38
- `${theme.fg("accent", "✦")} ${theme.bold("PieXis")} ${theme.fg("dim", "permission-aware coding workspace")} ${theme.fg("muted", project)}`,
39
- rule,
40
- `${theme.fg("muted", "Try:")} explain code · find bugs · review diffs · trace behavior ${theme.fg("muted", "Build:")} edit · refactor · test`,
41
- `${theme.fg("muted", "Run:")} tests · lint · typecheck · Git ${theme.fg("muted", "Plan:")} /plan <task> · Alt+M modes ${theme.fg("dim", "Mention files, constraints, and expected behavior.")}`
42
- ];
43
- return lines.map((line) => truncateToWidth(line, width, ""));
114
+ if (width <= 0) return [];
115
+
116
+ const left = width < WORDMARK_WIDTH
117
+ ? [`${theme.fg("accent", "✦")} ${theme.bold("PieXis")}`]
118
+ : WORDMARK.map((line) => theme.fg("accent", line));
119
+
120
+ const tips = [
121
+ theme.fg("accent", theme.bold("Get started with PieXis")),
122
+ theme.fg("muted", "Questions, skills, prompts & stats."),
123
+ ...agentSetupTips.map((tip) => theme.fg("warning", tip)),
124
+ ...selectedTips.map((tip) =>
125
+ `${theme.fg("accent", `[${tip.category}]`)} ${theme.fg("muted", tip.text)}`
126
+ ),
127
+ theme.fg("dim", "/help · /summary · /commit")
128
+ ];
129
+ const tipWidth = width - WORDMARK_WIDTH - 3;
130
+ if (tipWidth >= 32) {
131
+ const right = tips.flatMap((line) => wrapTextWithAnsi(line, tipWidth));
132
+ return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
133
+ const line = truncateToWidth(left[index] ?? "", WORDMARK_WIDTH, "");
134
+ const padding = " ".repeat(WORDMARK_WIDTH - visibleWidth(line) + 3);
135
+ return line + padding + (right[index] ?? "");
136
+ });
137
+ }
138
+
139
+ return [
140
+ ...left.map((line) => truncateToWidth(line, width, "")),
141
+ "",
142
+ ...tips.flatMap((line) => wrapTextWithAnsi(line, width))
143
+ ];
44
144
  },
45
145
  invalidate() {}
46
146
  }));
47
147
 
48
148
  ctx.ui.setFooter((tui, theme, footerData) => {
49
149
  redraw = () => tui.requestRender();
150
+ const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
50
151
 
51
- const unsubscribe = footerData.onBranchChange(() => {
52
- tui.requestRender();
53
- });
152
+ // Spinner animation, only while busy.
153
+ const timer = setInterval(() => {
154
+ if (state !== "ready") tui.requestRender();
155
+ }, 100);
156
+ timer.unref?.();
54
157
 
55
158
  return {
56
159
  invalidate() {},
57
160
 
58
161
  render(width: number) {
59
- const branch = footerData.getGitBranch() ?? "—";
162
+ if (width <= 0) return [];
163
+ const dim = (s: string) => theme.fg("dim", s);
164
+ const muted = (s: string) => theme.fg("muted", s);
165
+ const sep = dim(" · ");
166
+ const prompts = ctx.sessionManager.getEntries().filter(
167
+ (entry) => entry.type === "message" && entry.message.role === "user"
168
+ ).length;
60
169
 
61
- const model = ctx.model?.id?.replace(/^gpt-/, "") ?? "—";
62
- const effort = ctx.thinkingLevel ?? "off";
63
-
64
- const usage = ctx.getContextUsage();
65
-
66
- const context =
67
- usage?.percent != null
68
- ? `ctx ${Math.round(usage.percent)}%`
69
- : "ctx —";
170
+ let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
171
+ for (const entry of ctx.sessionManager.getBranch()) {
172
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
173
+ const u = entry.message.usage;
174
+ if (!u) continue;
175
+ input += u.input;
176
+ output += u.output;
177
+ cacheRead += u.cacheRead;
178
+ cacheWrite += u.cacheWrite;
179
+ cost += u.cost?.total ?? 0;
180
+ }
70
181
 
71
- let activity: string;
182
+ const frame = SPINNER[Math.floor(Date.now() / 100) % SPINNER.length];
183
+ const activity = state === "tool" ? `${frame} ${activeTool}`
184
+ : state === "thinking" ? `${frame} thinking` : "● ready";
185
+ const agent = footerData.getExtensionStatuses?.().get("piexis-agent");
186
+ const branch = footerData.getGitBranch();
187
+ // Reserve a small model slot; long tool names must not displace the profile.
188
+ const modelReserve = width >= 32 ? 9 : 0;
189
+ const profile = agent && visibleWidth(agent) + 6 + modelReserve <= width
190
+ ? theme.fg("accent", agent) + sep : "";
191
+ const activityWidth = Math.max(1, Math.min(24, width - visibleWidth(profile) - modelReserve));
192
+ const left1 = profile + theme.fg(state === "ready" ? "success" : "accent",
193
+ truncateToWidth(activity, activityWidth, "…"));
194
+ const row1 = justify(left1, [
195
+ { text: ctx.model?.id ?? "—", priority: 5 },
196
+ { text: muted(`think ${pi.getThinkingLevel()}`), priority: 4 },
197
+ ...(branch ? [{ text: dim(`⎇ ${truncateToWidth(branch, Math.max(8, Math.floor(width / 4)), "…")}`), priority: 3 }] : []),
198
+ { text: dim(`${prompts} prompts`), priority: 2 },
199
+ ...(agent ? [{ text: dim("[Alt+A]"), priority: 1 }] : [])
200
+ ], sep, width);
72
201
 
73
- if (state === "tool") {
74
- activity = theme.fg("accent", `◆ ${activeTool}`);
75
- } else if (state === "thinking") {
76
- activity = theme.fg("accent", "◆ thinking");
77
- } else {
78
- activity = theme.fg("success", "● ready");
79
- }
202
+ const usage = ctx.getContextUsage();
203
+ const pct = usage?.percent ?? null;
204
+ const tone = pct == null ? "dim" : pct >= 90 ? "error" : pct >= 70 ? "warning" : "success";
205
+ const pctText = pct == null ? "—" : `${Math.round(pct)}%`;
206
+ const costText = `$${cost < 0.01 ? cost.toFixed(4) : cost.toFixed(2)}`;
207
+ const barWidth = Math.min(12, Math.max(0,
208
+ width - visibleWidth(`ctx ${pctText}`) - visibleWidth(costText) - 2));
209
+ const filled = pct == null ? 0 : Math.round((Math.min(100, Math.max(0, pct)) / 100) * barWidth);
210
+ const bar = theme.fg(tone, "━".repeat(filled)) + dim("─".repeat(barWidth - filled));
211
+ const cap = usage
212
+ ? `${usage.tokens == null ? "—" : fmt(usage.tokens)}/${fmt(usage.contextWindow)}`
213
+ : "—/—";
214
+ const context = `${muted("ctx")} ${barWidth ? bar + " " : ""}${theme.fg(tone, pctText)}`;
215
+ const tokens = muted(`↑${fmt(input)} ↓${fmt(output)}`);
216
+ const promptTotal = input + cacheRead + cacheWrite;
217
+ const hit = promptTotal > 0 ? Math.round((cacheRead / promptTotal) * 100) : null;
218
+ // Add capacity only when it leaves room for the basic token totals and cost.
219
+ const showCapacity = visibleWidth(context + " " + cap + tokens + costText) + 7 <= width;
220
+ const left2 = context + (showCapacity ? " " + dim(cap) : "");
221
+ const row2 = justify(left2, [
222
+ { text: tokens, priority: 2 },
223
+ ...(hit == null ? [] : [{ text: dim(`cache ${hit}%`), priority: 1 }]),
224
+ { text: muted(costText), priority: 3 }
225
+ ], sep, width);
80
226
 
81
- const statuses = [...footerData.getExtensionStatuses().values()];
82
- // Keep this tolerant of theme/icon changes in the permission extension.
83
- const modeStatus =
84
- statuses.find((status) =>
85
- /(?:manual|accept edits|plan|auto|don't ask|bypass permissions).*on/i.test(
86
- status
87
- )
88
- ) ?? "";
89
- const line =
90
- `${activity}` +
91
- (modeStatus ? theme.fg("accent", ` │ ${modeStatus}`) : "") +
92
- theme.fg(
93
- "dim",
94
- ` │ ${branch}` + ` │ ${model}` + ` │ ${context}`
95
- );
96
-
97
- return [truncateToWidth(line, width, "")];
227
+ return [row1, row2];
98
228
  },
99
229
 
100
230
  dispose() {
231
+ clearInterval(timer);
101
232
  unsubscribe();
233
+ redraw = () => {};
102
234
  }
103
235
  };
104
236
  });
@@ -112,6 +244,20 @@ export default function (pi: ExtensionAPI) {
112
244
  update();
113
245
  });
114
246
 
247
+ pi.on("turn_start", async () => {
248
+ state = "thinking";
249
+ activeTool = "";
250
+ update();
251
+ });
252
+
253
+ pi.on("message_end", async () => {
254
+ update();
255
+ });
256
+
257
+ pi.on("session_compact", async () => {
258
+ update();
259
+ });
260
+
115
261
  pi.on("tool_call", async (event) => {
116
262
  state = "tool";
117
263
  activeTool = event.toolName;
@@ -0,0 +1,55 @@
1
+ export const agentNames = ["plan", "orchestrator", "debug", "ask", "code"] as const;
2
+ export type AgentName = typeof agentNames[number];
3
+ export const effortLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
4
+ export type Effort = typeof effortLevels[number];
5
+
6
+ export const agentConfigSchemaUrl = "https://raw.githubusercontent.com/VarunGaikwad/piexis/main/schemas/agent.schema.json";
7
+
8
+ export interface AgentConfig {
9
+ "$schema"?: string;
10
+ model?: string;
11
+ small_model?: string;
12
+ subagent_model?: string;
13
+ delegate_exploration?: boolean;
14
+ subagent_variant_overrides?: Partial<Record<AgentName, { model?: string; effort?: Effort }>>;
15
+ }
16
+
17
+ /**
18
+ * Static values written to a newly created .pi/agent.json.
19
+ * Add new static AgentConfig properties here so project setup includes them.
20
+ */
21
+ export const initialAgentConfigDefaults = {
22
+ "$schema": agentConfigSchemaUrl,
23
+ delegate_exploration: false,
24
+ } satisfies Pick<AgentConfig, "$schema" | "delegate_exploration">;
25
+
26
+ /** Header copy for incomplete project agent configuration. */
27
+ export type AgentConfigSetting = Exclude<keyof AgentConfig, "$schema">;
28
+
29
+ export const agentConfigCapabilities = {
30
+ model: "choose the default main model",
31
+ small_model: "use a cheaper model for eligible small tasks",
32
+ subagent_model: "choose the normal model for delegated work",
33
+ delegate_exploration: "opt in to cheap, read-only codebase exploration",
34
+ subagent_variant_overrides: "customize model and effort for each agent profile",
35
+ } satisfies Record<AgentConfigSetting, string>;
36
+
37
+ export function createInitialAgentConfig(selected: string, inherited: AgentConfig): AgentConfig {
38
+ const efforts: Record<AgentName, Effort> = { plan: "high", orchestrator: "high", debug: "high", ask: "low", code: "medium" };
39
+ const overrides: NonNullable<AgentConfig["subagent_variant_overrides"]> = {};
40
+ for (const name of agentNames) {
41
+ overrides[name] = {
42
+ model: inherited.subagent_model ?? selected,
43
+ effort: efforts[name],
44
+ ...inherited.subagent_variant_overrides?.[name],
45
+ };
46
+ }
47
+ return {
48
+ "$schema": initialAgentConfigDefaults.$schema,
49
+ model: selected,
50
+ small_model: inherited.small_model ?? selected,
51
+ subagent_model: inherited.subagent_model ?? selected,
52
+ delegate_exploration: inherited.delegate_exploration ?? initialAgentConfigDefaults.delegate_exploration,
53
+ subagent_variant_overrides: overrides,
54
+ };
55
+ }
package/lib/agents.ts ADDED
@@ -0,0 +1,208 @@
1
+ import { link, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
5
+ import { agentNames, createInitialAgentConfig, effortLevels, type AgentConfig, type AgentName, type Effort } from "./agent-config.ts";
6
+
7
+ export { agentNames, type AgentConfig, type AgentName, effortLevels, type Effort } from "./agent-config.ts";
8
+
9
+ export const profiles: Record<AgentName, string> = {
10
+ plan: "Explore the request and relevant code, clarify important unknowns, and return an actionable plan with acceptance checks. Do not implement or delegate implementation. Stop after the plan.",
11
+ orchestrator: "Break work into focused tasks and delegate to plan, debug, ask, or code using subagent. Include all necessary context in each task; children do not inherit the conversation. Keep overlapping file changes sequential. Inspect results, resolve disagreements, and report verification evidence. Do not delegate the same orchestration task recursively.",
12
+ debug: "Reproduce or establish evidence for the failure, trace its cause before editing, and make the smallest justified fix. Verify with a regression check. Separate confirmed facts from hypotheses and report any unexecuted checks.",
13
+ ask: "Answer the user's question using relevant code or documentation as evidence. Do not change files or delegate changes. When context is missing, ask rather than invent facts. Stop when the question is answered.",
14
+ code: "Implement the requested scope using existing repository patterns. Preserve unrelated changes, add focused coverage for behavior changes, run relevant checks, and report results. Stop when acceptance conditions are met.",
15
+ };
16
+
17
+ export function isAgentName(value: unknown): value is AgentName {
18
+ return typeof value === "string" && agentNames.includes(value as AgentName);
19
+ }
20
+
21
+ function object(value: unknown, label: string): Record<string, unknown> {
22
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
23
+ return value as Record<string, unknown>;
24
+ }
25
+
26
+ function model(value: unknown, label: string): string {
27
+ if (typeof value !== "string" || !/^[^\s/]+\/\S+$/.test(value)) {
28
+ throw new Error(`${label} must be an exact provider/model ID`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ export function parseAgentConfig(value: unknown): AgentConfig {
34
+ const source = object(value, "Agent configuration");
35
+ const result: AgentConfig = {};
36
+ for (const key of Object.keys(source)) {
37
+ if (!["$schema", "model", "small_model", "subagent_model", "delegate_exploration", "subagent_variant_overrides"].includes(key)) {
38
+ throw new Error(`Unknown agent configuration key: ${key}`);
39
+ }
40
+ }
41
+ if (source.$schema !== undefined) {
42
+ if (typeof source.$schema !== "string") throw new Error("$schema must be a string");
43
+ result.$schema = source.$schema;
44
+ }
45
+ for (const key of ["model", "small_model", "subagent_model"] as const) {
46
+ if (source[key] !== undefined) result[key] = model(source[key], key);
47
+ }
48
+ if (source.delegate_exploration !== undefined) {
49
+ if (typeof source.delegate_exploration !== "boolean") throw new Error("delegate_exploration must be a boolean");
50
+ result.delegate_exploration = source.delegate_exploration;
51
+ }
52
+ if (source.subagent_variant_overrides !== undefined) {
53
+ result.subagent_variant_overrides = {};
54
+ for (const [name, value] of Object.entries(object(source.subagent_variant_overrides, "subagent_variant_overrides"))) {
55
+ if (!isAgentName(name)) throw new Error(`Unknown agent variant: ${name}`);
56
+ const override = object(value, `Override ${name}`);
57
+ for (const key of Object.keys(override)) {
58
+ if (key !== "model" && key !== "effort") throw new Error(`Unknown override key: ${name}.${key}`);
59
+ }
60
+ const parsed: { model?: string; effort?: Effort } = {};
61
+ if (override.model !== undefined) parsed.model = model(override.model, `${name}.model`);
62
+ if (override.effort !== undefined) {
63
+ if (!effortLevels.includes(override.effort as Effort)) throw new Error(`Invalid effort for ${name}`);
64
+ parsed.effort = override.effort as Effort;
65
+ }
66
+ result.subagent_variant_overrides[name] = parsed;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ export async function loadAgentConfig(cwd: string, agentDir: string, trusted: boolean): Promise<AgentConfig> {
73
+ async function load(directory: string): Promise<AgentConfig> {
74
+ for (const name of ["agent.json", "agents.json"]) {
75
+ const path = join(directory, name);
76
+ try { return parseAgentConfig(JSON.parse(await readFile(path, "utf8"))); }
77
+ catch (error) {
78
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
79
+ throw new Error(`${path}: ${error instanceof Error ? error.message : String(error)}`);
80
+ }
81
+ }
82
+ return {};
83
+ }
84
+ const global = await load(agentDir);
85
+ const project = trusted ? await load(join(cwd, ".pi")) : {};
86
+ const overrides = { ...global.subagent_variant_overrides };
87
+ for (const name of agentNames) {
88
+ if (project.subagent_variant_overrides?.[name]) {
89
+ overrides[name] = { ...overrides[name], ...project.subagent_variant_overrides[name] };
90
+ }
91
+ }
92
+ return { ...global, ...project, subagent_variant_overrides: overrides };
93
+ }
94
+
95
+ export async function hasProjectAgentConfig(cwd: string): Promise<boolean> {
96
+ for (const name of ["agent.json", "agents.json"]) {
97
+ try { await lstat(join(cwd, ".pi", name)); return true; }
98
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
99
+ }
100
+ return false;
101
+ }
102
+
103
+ /** Create an opt-in project scaffold atomically, never replace existing or legacy configuration. */
104
+ export async function initializeAgentConfig(cwd: string, agentDir: string, defaultModel: string | undefined, signal?: AbortSignal): Promise<boolean> {
105
+ const directory = join(cwd, ".pi");
106
+ const path = join(directory, "agent.json");
107
+ return withFileMutationQueue(path, async () => {
108
+ signal?.throwIfAborted();
109
+ if (await hasProjectAgentConfig(cwd)) return false;
110
+ if (!defaultModel) throw new Error("No active model; select a model and create .pi/agent.json manually.");
111
+ const inherited = await loadAgentConfig(cwd, agentDir, false);
112
+ const selected = inherited.model ?? model(defaultModel, "Default model");
113
+ const config = createInitialAgentConfig(selected, inherited);
114
+ signal?.throwIfAborted();
115
+ await mkdir(directory, { recursive: true });
116
+ const temporary = join(directory, `.agent.json.piexis-${randomUUID()}.tmp`);
117
+ try {
118
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { flag: "wx", mode: 0o600 });
119
+ signal?.throwIfAborted();
120
+ if (await hasProjectAgentConfig(cwd)) return false;
121
+ // Unlike rename, link fails if another writer has already created the destination.
122
+ try { await link(temporary, path); }
123
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") return false; throw error; }
124
+ return true;
125
+ } finally { await rm(temporary, { force: true }); }
126
+ });
127
+ }
128
+
129
+ export async function completeProjectAgentConfig(cwd: string, agentDir: string, defaultModel: string | undefined, signal?: AbortSignal): Promise<"created" | "updated" | "complete"> {
130
+ const directory = join(cwd, ".pi");
131
+ let path: string | undefined;
132
+ for (const name of ["agent.json", "agents.json"]) {
133
+ const candidate = join(directory, name);
134
+ try { await lstat(candidate); path = candidate; break; }
135
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
136
+ }
137
+ if (!path) {
138
+ if (!await initializeAgentConfig(cwd, agentDir, defaultModel, signal)) return "complete";
139
+ return "created";
140
+ }
141
+ return withFileMutationQueue(path, async () => {
142
+ signal?.throwIfAborted();
143
+ if (!(await lstat(path!)).isFile()) throw new Error(`${path} must be a regular file`);
144
+ const source = parseAgentConfig(JSON.parse(await readFile(path!, "utf8")));
145
+ const inherited = await loadAgentConfig(cwd, agentDir, false);
146
+ const selected = source.model ?? inherited.model ?? (defaultModel ? model(defaultModel, "Default model") : undefined);
147
+ if (!selected) throw new Error("No active model; select a model and configure .pi/agent.json manually.");
148
+ const defaults = createInitialAgentConfig(selected, inherited);
149
+ const overrides: NonNullable<AgentConfig["subagent_variant_overrides"]> = {};
150
+ let changed = Object.keys(defaults).some((key) => source[key as keyof AgentConfig] === undefined);
151
+ for (const name of agentNames) {
152
+ const expected = defaults.subagent_variant_overrides![name]!;
153
+ const actual = source.subagent_variant_overrides?.[name];
154
+ if (!actual?.model || actual.effort === undefined) changed = true;
155
+ overrides[name] = { ...expected, ...actual };
156
+ }
157
+ if (!changed) return "complete";
158
+ const config: AgentConfig = { ...defaults, ...source, subagent_variant_overrides: overrides };
159
+ const temporary = join(directory, `.${basename(path!)}.piexis-${randomUUID()}.tmp`);
160
+ try {
161
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { flag: "wx", mode: 0o600 });
162
+ signal?.throwIfAborted();
163
+ await rename(temporary, path!);
164
+ return "updated";
165
+ } finally { await rm(temporary, { force: true }); }
166
+ });
167
+ }
168
+
169
+ export function isSmallTask(agent: AgentName, task: string): boolean {
170
+ // shortcut: conservative text heuristic, replace with evaluated routing if real tasks outgrow it.
171
+ if (task.length > 400 || task.split(/\s+/).length > 65) return false;
172
+ if (/\b(architect\w*|design|implement|investigate|debug|refactor|migrat\w*|security|race|performance|across|multiple|entire|comprehensive)\b/i.test(task)) return false;
173
+ if (agent === "ask") return /^\s*(find|locate|list|what (is|does|file)|where (is|are)|explain|summari[sz]e)\b/i.test(task);
174
+ if (agent === "code") return /^\s*(fix (a |the )?typo|correct (a |the )?typo|rename (a |the )?(variable|symbol)|update (a |the )?(comment|label))\b/i.test(task);
175
+ return false;
176
+ }
177
+
178
+ export function routeSubagent(config: AgentConfig, agent: AgentName, task: string, currentModel: string, currentEffort: Effort, exploration = false) {
179
+ if (exploration) {
180
+ if (!config.small_model) throw new Error("Exploration delegation requires small_model in agent.json");
181
+ return { model: config.small_model, effort: "low" as Effort, reason: "configured exploration small model" };
182
+ }
183
+ const override = config.subagent_variant_overrides?.[agent];
184
+ const small = Boolean(config.small_model && isSmallTask(agent, task) && !override?.model);
185
+ return {
186
+ model: override?.model ?? (small ? config.small_model! : config.subagent_model ?? currentModel),
187
+ effort: override?.effort ?? (small ? "low" : currentEffort),
188
+ reason: override?.model ? "variant override" : small ? "automatic small task" : "default subagent",
189
+ };
190
+ }
191
+
192
+ export function subagentTools(agent: AgentName, active: string[], depth: number, allowExplorationDelegation = false): string[] {
193
+ if (agent === "plan" || agent === "ask") {
194
+ return active.filter((name) => ["read", "grep", "find", "ls"].includes(name) || (allowExplorationDelegation && depth < 2 && name === "subagent"));
195
+ }
196
+ return active.filter((name) => name !== "AskQuestion" && (name !== "subagent" || depth < 2));
197
+ }
198
+
199
+ export function childResourceArgs(argv: string[]): string[] {
200
+ const result: string[] = [];
201
+ const values = ["--extension", "-e", "--skill", "--prompt-template", "--system-prompt", "--append-system-prompt"];
202
+ const flags = ["--no-extensions", "-ne", "--no-skills", "-ns", "--no-prompt-templates", "-np", "--no-context-files", "-nc", "--no-mcp", "--offline"];
203
+ for (let i = 0; i < argv.length && argv[i] !== "--"; i++) {
204
+ if (values.includes(argv[i]) && argv[i + 1] !== undefined) result.push(argv[i], argv[++i]);
205
+ else if (flags.includes(argv[i])) result.push(argv[i]);
206
+ }
207
+ return result;
208
+ }