@preapexis/piexis 0.2.2 → 0.2.5

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 (149) hide show
  1. package/README.md +85 -131
  2. package/apps/server/src/index.ts +97 -0
  3. package/apps/server/src/session-manager.ts +95 -0
  4. package/apps/server/src/workspace.ts +39 -0
  5. package/{tsconfig.json → apps/server/tsconfig.json} +3 -3
  6. package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
  7. package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
  8. package/apps/web/dist/index.html +3 -0
  9. package/bin/piexis-web.mjs +24 -0
  10. package/extensions/agents.ts +205 -0
  11. package/extensions/ask-question.ts +54 -95
  12. package/extensions/recommended-tools.ts +62 -0
  13. package/extensions/ui.ts +204 -58
  14. package/extensions/webui.ts +45 -0
  15. package/lib/agent-config.ts +55 -0
  16. package/lib/agents.ts +208 -0
  17. package/lib/question-model.ts +98 -0
  18. package/lib/question-ui.ts +214 -0
  19. package/lib/recommended-tools.ts +119 -0
  20. package/lib/subagent-runner.ts +146 -0
  21. package/lib/ui-tips.json +41 -0
  22. package/package.json +41 -14
  23. package/packages/pi-integration/src/rpc-client.ts +99 -0
  24. package/packages/shared/src/index.ts +27 -0
  25. package/prompts/init.md +10 -2
  26. package/prompts/summary.md +7 -6
  27. package/schemas/agent.schema.json +48 -0
  28. package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
  29. package/skills/caveman/LICENSE-MIT +21 -0
  30. package/skills/caveman/NOTICE +10 -0
  31. package/skills/caveman/SKILL.md +89 -0
  32. package/skills/find-skills/SKILL.md +141 -0
  33. package/skills/ponytail/LICENSE +21 -0
  34. package/skills/ponytail/NOTICE +9 -0
  35. package/skills/ponytail/SKILL.md +52 -0
  36. package/themes/midnight.json +81 -0
  37. package/.github/workflows/publish.yml +0 -35
  38. package/AGENTS.md +0 -58
  39. package/eval/README.md +0 -72
  40. package/eval/permission-cases.json +0 -28
  41. package/eval/permissions.mjs +0 -103
  42. package/extensions/background-tasks.ts +0 -433
  43. package/extensions/background-worker.ts +0 -50
  44. package/extensions/glob.ts +0 -32
  45. package/extensions/mode.ts +0 -397
  46. package/lib/bash-approvals.ts +0 -113
  47. package/lib/bash-policy.ts +0 -164
  48. package/lib/mode-policy.ts +0 -174
  49. package/lib/path-policy.ts +0 -182
  50. package/lib/permission-classifier.ts +0 -128
  51. package/lib/permission-config.ts +0 -91
  52. package/lib/permission-denials.ts +0 -32
  53. package/lib/permission-intent.ts +0 -128
  54. package/lib/permission-taxonomy.ts +0 -15
  55. package/lib/task-broker.ts +0 -65
  56. package/lib/task-checkout.ts +0 -39
  57. package/lib/task-git.ts +0 -44
  58. package/lib/task-patch.ts +0 -106
  59. package/lib/task-permissions.ts +0 -51
  60. package/lib/task-report.ts +0 -62
  61. package/lib/task-worktree-policy.ts +0 -56
  62. package/scripts/release.mjs +0 -32
  63. package/skills/claude-api/SKILL.md +0 -572
  64. package/skills/claude-api/csharp/claude-api/README.md +0 -362
  65. package/skills/claude-api/csharp/claude-api/batches.md +0 -14
  66. package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
  67. package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
  68. package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
  69. package/skills/claude-api/curl/examples.md +0 -256
  70. package/skills/claude-api/curl/managed-agents.md +0 -371
  71. package/skills/claude-api/go/claude-api/README.md +0 -188
  72. package/skills/claude-api/go/claude-api/files-api.md +0 -23
  73. package/skills/claude-api/go/claude-api/streaming.md +0 -43
  74. package/skills/claude-api/go/claude-api/tool-use.md +0 -220
  75. package/skills/claude-api/go/managed-agents/README.md +0 -564
  76. package/skills/claude-api/java/claude-api/README.md +0 -238
  77. package/skills/claude-api/java/claude-api/files-api.md +0 -27
  78. package/skills/claude-api/java/claude-api/streaming.md +0 -24
  79. package/skills/claude-api/java/claude-api/tool-use.md +0 -228
  80. package/skills/claude-api/java/managed-agents/README.md +0 -443
  81. package/skills/claude-api/php/claude-api/README.md +0 -174
  82. package/skills/claude-api/php/claude-api/batches.md +0 -15
  83. package/skills/claude-api/php/claude-api/files-api.md +0 -13
  84. package/skills/claude-api/php/claude-api/streaming.md +0 -27
  85. package/skills/claude-api/php/claude-api/tool-use.md +0 -253
  86. package/skills/claude-api/php/managed-agents/README.md +0 -443
  87. package/skills/claude-api/python/claude-api/README.md +0 -565
  88. package/skills/claude-api/python/claude-api/batches.md +0 -198
  89. package/skills/claude-api/python/claude-api/files-api.md +0 -170
  90. package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
  91. package/skills/claude-api/python/claude-api/streaming.md +0 -179
  92. package/skills/claude-api/python/claude-api/tool-use.md +0 -627
  93. package/skills/claude-api/python/managed-agents/README.md +0 -335
  94. package/skills/claude-api/ruby/claude-api/README.md +0 -134
  95. package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
  96. package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
  97. package/skills/claude-api/ruby/managed-agents/README.md +0 -394
  98. package/skills/claude-api/shared/admin-api.md +0 -179
  99. package/skills/claude-api/shared/agent-design.md +0 -101
  100. package/skills/claude-api/shared/anthropic-cli.md +0 -269
  101. package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
  102. package/skills/claude-api/shared/cost-optimization.md +0 -233
  103. package/skills/claude-api/shared/error-codes.md +0 -262
  104. package/skills/claude-api/shared/live-sources.md +0 -166
  105. package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
  106. package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
  107. package/skills/claude-api/shared/managed-agents-core.md +0 -359
  108. package/skills/claude-api/shared/managed-agents-environments.md +0 -226
  109. package/skills/claude-api/shared/managed-agents-events.md +0 -290
  110. package/skills/claude-api/shared/managed-agents-memory.md +0 -201
  111. package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
  112. package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
  113. package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
  114. package/skills/claude-api/shared/managed-agents-overview.md +0 -77
  115. package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
  116. package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
  117. package/skills/claude-api/shared/managed-agents-tools.md +0 -504
  118. package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
  119. package/skills/claude-api/shared/model-migration.md +0 -1872
  120. package/skills/claude-api/shared/models.md +0 -142
  121. package/skills/claude-api/shared/platform-availability.md +0 -53
  122. package/skills/claude-api/shared/prompt-audit.md +0 -223
  123. package/skills/claude-api/shared/prompt-caching.md +0 -292
  124. package/skills/claude-api/shared/token-counting.md +0 -56
  125. package/skills/claude-api/shared/tool-use-concepts.md +0 -526
  126. package/skills/claude-api/typescript/claude-api/README.md +0 -405
  127. package/skills/claude-api/typescript/claude-api/batches.md +0 -106
  128. package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
  129. package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
  130. package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
  131. package/skills/claude-api/typescript/managed-agents/README.md +0 -359
  132. package/skills-lock.json +0 -359
  133. package/tests/auto-permissions.test.mjs +0 -315
  134. package/tests/background-tasks.test.mjs +0 -27
  135. package/tests/background-worker.test.mjs +0 -60
  136. package/tests/bash-permissions.test.mjs +0 -276
  137. package/tests/glob.test.mjs +0 -31
  138. package/tests/mode.test.mjs +0 -62
  139. package/tests/permission-evaluation.test.mjs +0 -70
  140. package/tests/permission-foundation.test.mjs +0 -194
  141. package/tests/permission-policy.test.mjs +0 -158
  142. package/tests/runtime/README.md +0 -51
  143. package/tests/runtime/harness.mjs +0 -190
  144. package/tests/runtime/permissions.test.mjs +0 -192
  145. package/tests/runtime/tui.test.mjs +0 -60
  146. package/tests/skills-routing.test.mjs +0 -51
  147. package/tests/task-integration.test.mjs +0 -324
  148. package/tests/task-permissions.test.mjs +0 -178
  149. 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,45 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { spawn, type ChildProcess } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createServer } from "node:net";
5
+ import path from "node:path";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+
8
+ async function availablePort(): Promise<number> {
9
+ const server = createServer();
10
+ await new Promise<void>((resolve, reject) => {
11
+ server.once("error", reject);
12
+ server.listen(0, "127.0.0.1", () => resolve());
13
+ });
14
+ const address = server.address();
15
+ await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
16
+ if (!address || typeof address === "string") throw new Error("Could not select a local PIXIS WebUI port.");
17
+ return address.port;
18
+ }
19
+
20
+ /** Starts the separate local WebUI; Pi's TUI remains usable in this process. */
21
+ export default function webui(pi: ExtensionAPI) {
22
+ const servers = new Map<string, ChildProcess>();
23
+ const launcher = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../bin/piexis-web.mjs");
24
+
25
+ pi.registerCommand("webui", {
26
+ description: "Start the local PIXIS WebUI for this workspace",
27
+ handler: async (_args, ctx) => {
28
+ const running = servers.get(ctx.cwd);
29
+ if (running && running.exitCode === null && !running.killed) {
30
+ ctx.ui.notify("PIXIS WebUI is already running for this workspace.", "info");
31
+ return;
32
+ }
33
+ const token = randomBytes(32).toString("base64url");
34
+ const port = await availablePort();
35
+ const child = spawn(process.execPath, [launcher, "--workspace", ctx.cwd, "--port", `${port}`, "--token", token], {
36
+ detached: true,
37
+ stdio: "ignore",
38
+ windowsHide: true
39
+ });
40
+ child.unref();
41
+ servers.set(ctx.cwd, child);
42
+ ctx.ui.notify(`PIXIS WebUI started: http://127.0.0.1:${port}/#token=${token}`, "info");
43
+ }
44
+ });
45
+ }
@@ -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
+ }