@chankov/agent-skills 0.3.2 → 0.3.3
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/.versions/0.3.3/.claude/commands/build.md +18 -0
- package/.versions/0.3.3/.claude/commands/code-simplify.md +22 -0
- package/.versions/0.3.3/.claude/commands/design-agent.md +14 -0
- package/.versions/0.3.3/.claude/commands/doctor-agent-skills.md +13 -0
- package/.versions/0.3.3/.claude/commands/plan.md +16 -0
- package/.versions/0.3.3/.claude/commands/prime.md +22 -0
- package/.versions/0.3.3/.claude/commands/review.md +16 -0
- package/.versions/0.3.3/.claude/commands/setup-agent-skills.md +19 -0
- package/.versions/0.3.3/.claude/commands/ship.md +17 -0
- package/.versions/0.3.3/.claude/commands/spec.md +15 -0
- package/.versions/0.3.3/.claude/commands/test.md +19 -0
- package/.versions/0.3.3/.opencode/commands/as-build.md +17 -0
- package/.versions/0.3.3/.opencode/commands/as-code-simplify.md +16 -0
- package/.versions/0.3.3/.opencode/commands/as-design-agent.md +15 -0
- package/.versions/0.3.3/.opencode/commands/as-doctor-agent-skills.md +11 -0
- package/.versions/0.3.3/.opencode/commands/as-plan.md +16 -0
- package/.versions/0.3.3/.opencode/commands/as-prime.md +22 -0
- package/.versions/0.3.3/.opencode/commands/as-review.md +15 -0
- package/.versions/0.3.3/.opencode/commands/as-setup-agent-skills.md +11 -0
- package/.versions/0.3.3/.opencode/commands/as-ship.md +16 -0
- package/.versions/0.3.3/.opencode/commands/as-spec.md +16 -0
- package/.versions/0.3.3/.opencode/commands/as-test.md +21 -0
- package/.versions/0.3.3/.pi/agents/agent-chain.yaml +49 -0
- package/.versions/0.3.3/.pi/agents/bowser.md +19 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/agent-expert.md +98 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/cli-expert.md +41 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/config-expert.md +63 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/ext-expert.md +43 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/keybinding-expert.md +134 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/pi-orchestrator.md +57 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/prompt-expert.md +70 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/skill-expert.md +42 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/theme-expert.md +40 -0
- package/.versions/0.3.3/.pi/agents/pi-pi/tui-expert.md +85 -0
- package/.versions/0.3.3/.pi/agents/teams.yaml +31 -0
- package/.versions/0.3.3/.pi/damage-control-rules.yaml +278 -0
- package/.versions/0.3.3/.pi/extensions/agent-skills-update-check/README.md +58 -0
- package/.versions/0.3.3/.pi/extensions/agent-skills-update-check/index.ts +161 -0
- package/.versions/0.3.3/.pi/extensions/agent-skills-update-check/package.json +6 -0
- package/.versions/0.3.3/.pi/extensions/chrome-devtools-mcp/README.md +39 -0
- package/.versions/0.3.3/.pi/extensions/chrome-devtools-mcp/index.ts +61 -0
- package/.versions/0.3.3/.pi/extensions/chrome-devtools-mcp/package.json +6 -0
- package/.versions/0.3.3/.pi/extensions/compact-and-continue/README.md +42 -0
- package/.versions/0.3.3/.pi/extensions/compact-and-continue/index.ts +120 -0
- package/.versions/0.3.3/.pi/extensions/compact-and-continue/package.json +6 -0
- package/.versions/0.3.3/.pi/extensions/mcp-bridge/README.md +46 -0
- package/.versions/0.3.3/.pi/extensions/mcp-bridge/index.ts +206 -0
- package/.versions/0.3.3/.pi/extensions/mcp-bridge/package.json +6 -0
- package/.versions/0.3.3/.pi/extensions/package-lock.json +1143 -0
- package/.versions/0.3.3/.pi/extensions/package.json +9 -0
- package/.versions/0.3.3/.pi/harnesses/agent-chain/README.md +37 -0
- package/.versions/0.3.3/.pi/harnesses/agent-chain/index.ts +795 -0
- package/.versions/0.3.3/.pi/harnesses/agent-chain/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/agent-team/README.md +38 -0
- package/.versions/0.3.3/.pi/harnesses/agent-team/index.ts +732 -0
- package/.versions/0.3.3/.pi/harnesses/agent-team/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/coms/README.md +36 -0
- package/.versions/0.3.3/.pi/harnesses/coms/index.ts +1595 -0
- package/.versions/0.3.3/.pi/harnesses/coms/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/coms-net/README.md +46 -0
- package/.versions/0.3.3/.pi/harnesses/coms-net/index.ts +1637 -0
- package/.versions/0.3.3/.pi/harnesses/coms-net/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/damage-control/README.md +38 -0
- package/.versions/0.3.3/.pi/harnesses/damage-control/index.ts +207 -0
- package/.versions/0.3.3/.pi/harnesses/damage-control/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/damage-control-continue/README.md +37 -0
- package/.versions/0.3.3/.pi/harnesses/damage-control-continue/index.ts +234 -0
- package/.versions/0.3.3/.pi/harnesses/damage-control-continue/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/minimal/README.md +27 -0
- package/.versions/0.3.3/.pi/harnesses/minimal/index.ts +32 -0
- package/.versions/0.3.3/.pi/harnesses/minimal/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/package-lock.json +35 -0
- package/.versions/0.3.3/.pi/harnesses/package.json +9 -0
- package/.versions/0.3.3/.pi/harnesses/pi-pi/README.md +39 -0
- package/.versions/0.3.3/.pi/harnesses/pi-pi/index.ts +631 -0
- package/.versions/0.3.3/.pi/harnesses/pi-pi/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/purpose-gate/README.md +27 -0
- package/.versions/0.3.3/.pi/harnesses/purpose-gate/index.ts +82 -0
- package/.versions/0.3.3/.pi/harnesses/purpose-gate/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/session-replay/README.md +28 -0
- package/.versions/0.3.3/.pi/harnesses/session-replay/index.ts +214 -0
- package/.versions/0.3.3/.pi/harnesses/session-replay/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/subagent-widget/README.md +36 -0
- package/.versions/0.3.3/.pi/harnesses/subagent-widget/index.ts +479 -0
- package/.versions/0.3.3/.pi/harnesses/subagent-widget/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/system-select/README.md +39 -0
- package/.versions/0.3.3/.pi/harnesses/system-select/index.ts +165 -0
- package/.versions/0.3.3/.pi/harnesses/system-select/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/tilldone/README.md +35 -0
- package/.versions/0.3.3/.pi/harnesses/tilldone/index.ts +724 -0
- package/.versions/0.3.3/.pi/harnesses/tilldone/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/tool-counter/README.md +31 -0
- package/.versions/0.3.3/.pi/harnesses/tool-counter/index.ts +100 -0
- package/.versions/0.3.3/.pi/harnesses/tool-counter/package.json +6 -0
- package/.versions/0.3.3/.pi/harnesses/tool-counter-widget/README.md +27 -0
- package/.versions/0.3.3/.pi/harnesses/tool-counter-widget/index.ts +66 -0
- package/.versions/0.3.3/.pi/harnesses/tool-counter-widget/package.json +6 -0
- package/.versions/0.3.3/.pi/prompts/build.md +24 -0
- package/.versions/0.3.3/.pi/prompts/code-simplify.md +22 -0
- package/.versions/0.3.3/.pi/prompts/doctor-agent-skills.md +13 -0
- package/.versions/0.3.3/.pi/prompts/plan.md +16 -0
- package/.versions/0.3.3/.pi/prompts/review.md +16 -0
- package/.versions/0.3.3/.pi/prompts/setup-agent-skills.md +19 -0
- package/.versions/0.3.3/.pi/prompts/ship.md +17 -0
- package/.versions/0.3.3/.pi/prompts/spec.md +15 -0
- package/.versions/0.3.3/.pi/prompts/test.md +19 -0
- package/.versions/0.3.3/.pi/skills/bowser/SKILL.md +114 -0
- package/.versions/0.3.3/.version +1 -0
- package/.versions/0.3.3/agents/builder.md +6 -0
- package/.versions/0.3.3/agents/code-reviewer.md +93 -0
- package/.versions/0.3.3/agents/documenter.md +6 -0
- package/.versions/0.3.3/agents/plan-reviewer.md +22 -0
- package/.versions/0.3.3/agents/planner.md +6 -0
- package/.versions/0.3.3/agents/scout.md +6 -0
- package/.versions/0.3.3/agents/security-auditor.md +97 -0
- package/.versions/0.3.3/agents/test-engineer.md +89 -0
- package/.versions/0.3.3/hooks/SIMPLIFY-IGNORE.md +90 -0
- package/.versions/0.3.3/hooks/hooks.json +14 -0
- package/.versions/0.3.3/hooks/session-start.sh +74 -0
- package/.versions/0.3.3/hooks/simplify-ignore-test.sh +247 -0
- package/.versions/0.3.3/hooks/simplify-ignore.sh +302 -0
- package/.versions/0.3.3/references/accessibility-checklist.md +159 -0
- package/.versions/0.3.3/references/performance-checklist.md +121 -0
- package/.versions/0.3.3/references/prompting-patterns.md +380 -0
- package/.versions/0.3.3/references/security-checklist.md +134 -0
- package/.versions/0.3.3/references/testing-patterns.md +236 -0
- package/.versions/0.3.3/scripts/coms-net-server.ts +1741 -0
- package/.versions/0.3.3/skills/api-and-interface-design/SKILL.md +294 -0
- package/.versions/0.3.3/skills/browser-testing-with-devtools/SKILL.md +335 -0
- package/.versions/0.3.3/skills/ci-cd-and-automation/SKILL.md +390 -0
- package/.versions/0.3.3/skills/code-review-and-quality/SKILL.md +347 -0
- package/.versions/0.3.3/skills/code-simplification/SKILL.md +331 -0
- package/.versions/0.3.3/skills/context-engineering/SKILL.md +291 -0
- package/.versions/0.3.3/skills/debugging-and-error-recovery/SKILL.md +300 -0
- package/.versions/0.3.3/skills/deprecation-and-migration/SKILL.md +206 -0
- package/.versions/0.3.3/skills/designing-agents/SKILL.md +394 -0
- package/.versions/0.3.3/skills/designing-agents/pi-harness-authoring.md +213 -0
- package/.versions/0.3.3/skills/documentation-and-adrs/SKILL.md +278 -0
- package/.versions/0.3.3/skills/frontend-ui-engineering/SKILL.md +322 -0
- package/.versions/0.3.3/skills/git-workflow-and-versioning/SKILL.md +316 -0
- package/.versions/0.3.3/skills/guided-workspace-setup/SKILL.md +345 -0
- package/.versions/0.3.3/skills/idea-refine/SKILL.md +178 -0
- package/.versions/0.3.3/skills/idea-refine/examples.md +238 -0
- package/.versions/0.3.3/skills/idea-refine/frameworks.md +99 -0
- package/.versions/0.3.3/skills/idea-refine/refinement-criteria.md +113 -0
- package/.versions/0.3.3/skills/idea-refine/scripts/idea-refine.sh +15 -0
- package/.versions/0.3.3/skills/incremental-implementation/SKILL.md +279 -0
- package/.versions/0.3.3/skills/performance-optimization/SKILL.md +350 -0
- package/.versions/0.3.3/skills/planning-and-task-breakdown/SKILL.md +237 -0
- package/.versions/0.3.3/skills/security-and-hardening/SKILL.md +349 -0
- package/.versions/0.3.3/skills/shipping-and-launch/SKILL.md +309 -0
- package/.versions/0.3.3/skills/source-driven-development/SKILL.md +194 -0
- package/.versions/0.3.3/skills/spec-driven-development/SKILL.md +237 -0
- package/.versions/0.3.3/skills/test-driven-development/SKILL.md +379 -0
- package/.versions/0.3.3/skills/using-agent-skills/SKILL.md +176 -0
- package/CHANGELOG.md +17 -0
- package/bin/snapshot-version.js +8 -1
- package/package.json +2 -1
- package/scripts/coms-net-server.ts +1741 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pi-pi
|
|
2
|
+
|
|
3
|
+
Meta-agent that builds pi agents.
|
|
4
|
+
|
|
5
|
+
> Ported from [`pi-vs-claude-code`](https://github.com/disler/pi-vs-claude-code) by [disler](https://github.com/disler) (MIT). See the [extension catalog](../../../docs/pi-extensions.md).
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
A team of domain-specific research experts — extensions, themes, skills, settings, TUI —
|
|
10
|
+
operate **in parallel** to gather pi documentation and patterns. The primary agent
|
|
11
|
+
synthesises their findings and is the only writer; the experts are read-only researchers.
|
|
12
|
+
Each expert fetches fresh pi documentation via Firecrawl on its first query.
|
|
13
|
+
|
|
14
|
+
Use it to scaffold new pi extensions, themes, skills, or agent definitions grounded in
|
|
15
|
+
current pi docs.
|
|
16
|
+
|
|
17
|
+
## Commands & tools
|
|
18
|
+
|
|
19
|
+
- `/experts` — list the available experts and their status
|
|
20
|
+
- `/experts-grid N` — set the dashboard column count (default 3)
|
|
21
|
+
|
|
22
|
+
## Requires
|
|
23
|
+
|
|
24
|
+
- `.pi/agents/pi-pi/*.md` — the expert persona definitions (shipped in this repo)
|
|
25
|
+
- `FIRECRAWL_API_KEY` — environment variable; experts use Firecrawl to crawl pi docs
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export FIRECRAWL_API_KEY=fc-...
|
|
31
|
+
pi -e .pi/harnesses/pi-pi/index.ts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Upstream changes
|
|
35
|
+
|
|
36
|
+
- Theme integration removed — the `themeMap.ts` import and the `applyExtensionDefaults()`
|
|
37
|
+
call were stripped (this repo does not ship pi themes). Note: pi-pi still has a
|
|
38
|
+
`theme-expert` research persona — that is unrelated to the stripped UI theming and is
|
|
39
|
+
about *researching* how pi themes are authored.
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Pi — Meta-agent that builds Pi agents
|
|
3
|
+
*
|
|
4
|
+
* A team of domain-specific research experts (extensions, themes, skills,
|
|
5
|
+
* settings, TUI) operate in PARALLEL to gather documentation and patterns.
|
|
6
|
+
* The primary agent synthesizes their findings and WRITES the actual files.
|
|
7
|
+
*
|
|
8
|
+
* Each expert fetches fresh Pi documentation via firecrawl on first query.
|
|
9
|
+
* Experts are read-only researchers. The primary agent is the only writer.
|
|
10
|
+
*
|
|
11
|
+
* Commands:
|
|
12
|
+
* /experts — list available experts and their status
|
|
13
|
+
* /experts-grid N — set dashboard column count (default 3)
|
|
14
|
+
*
|
|
15
|
+
* Usage: pi -e extensions/pi-pi.ts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import { Type } from "@sinclair/typebox";
|
|
20
|
+
import { Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
21
|
+
import { spawn } from "child_process";
|
|
22
|
+
import { readdirSync, readFileSync, existsSync, mkdirSync } from "fs";
|
|
23
|
+
import { join, resolve } from "path";
|
|
24
|
+
|
|
25
|
+
// ── Types ────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
interface ExpertDef {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
tools: string;
|
|
31
|
+
systemPrompt: string;
|
|
32
|
+
file: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ExpertState {
|
|
36
|
+
def: ExpertDef;
|
|
37
|
+
status: "idle" | "researching" | "done" | "error";
|
|
38
|
+
question: string;
|
|
39
|
+
elapsed: number;
|
|
40
|
+
lastLine: string;
|
|
41
|
+
queryCount: number;
|
|
42
|
+
timer?: ReturnType<typeof setInterval>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Helpers ──────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function displayName(name: string): string {
|
|
48
|
+
return name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseAgentFile(filePath: string): ExpertDef | null {
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
54
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
|
|
57
|
+
const frontmatter: Record<string, string> = {};
|
|
58
|
+
for (const line of match[1].split("\n")) {
|
|
59
|
+
const idx = line.indexOf(":");
|
|
60
|
+
if (idx > 0) {
|
|
61
|
+
frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!frontmatter.name) return null;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
name: frontmatter.name,
|
|
69
|
+
description: frontmatter.description || "",
|
|
70
|
+
tools: frontmatter.tools || "read,grep,find,ls",
|
|
71
|
+
systemPrompt: match[2].trim(),
|
|
72
|
+
file: filePath,
|
|
73
|
+
};
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Expert card colors ────────────────────────────
|
|
80
|
+
// Each expert gets a unique hue: bg fills the card interior,
|
|
81
|
+
// br is the matching border foreground (brighter shade of same hue).
|
|
82
|
+
const EXPERT_COLORS: Record<string, { bg: string; br: string }> = {
|
|
83
|
+
"agent-expert": { bg: "\x1b[48;2;20;30;75m", br: "\x1b[38;2;70;110;210m" }, // navy
|
|
84
|
+
"config-expert": { bg: "\x1b[48;2;18;65;30m", br: "\x1b[38;2;55;175;90m" }, // forest
|
|
85
|
+
"ext-expert": { bg: "\x1b[48;2;80;18;28m", br: "\x1b[38;2;210;65;85m" }, // crimson
|
|
86
|
+
"keybinding-expert": { bg: "\x1b[48;2;50;22;85m", br: "\x1b[38;2;145;80;220m" }, // violet
|
|
87
|
+
"prompt-expert": { bg: "\x1b[48;2;80;55;12m", br: "\x1b[38;2;215;150;40m" }, // amber
|
|
88
|
+
"skill-expert": { bg: "\x1b[48;2;12;65;75m", br: "\x1b[38;2;40;175;195m" }, // teal
|
|
89
|
+
"theme-expert": { bg: "\x1b[48;2;80;18;62m", br: "\x1b[38;2;210;55;160m" }, // rose
|
|
90
|
+
"tui-expert": { bg: "\x1b[48;2;28;42;80m", br: "\x1b[38;2;85;120;210m" }, // slate
|
|
91
|
+
"cli-expert": { bg: "\x1b[48;2;60;80;20m", br: "\x1b[38;2;160;210;55m" }, // olive/lime
|
|
92
|
+
};
|
|
93
|
+
const FG_RESET = "\x1b[39m";
|
|
94
|
+
const BG_RESET = "\x1b[49m";
|
|
95
|
+
|
|
96
|
+
// ── Extension ────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export default function (pi: ExtensionAPI) {
|
|
99
|
+
const experts: Map<string, ExpertState> = new Map();
|
|
100
|
+
let gridCols = 3;
|
|
101
|
+
let widgetCtx: any;
|
|
102
|
+
|
|
103
|
+
function loadExperts(cwd: string) {
|
|
104
|
+
// Pi Pi experts live in their own dedicated directory
|
|
105
|
+
const piPiDir = join(cwd, ".pi", "agents", "pi-pi");
|
|
106
|
+
|
|
107
|
+
experts.clear();
|
|
108
|
+
|
|
109
|
+
if (!existsSync(piPiDir)) return;
|
|
110
|
+
try {
|
|
111
|
+
for (const file of readdirSync(piPiDir)) {
|
|
112
|
+
if (!file.endsWith(".md")) continue;
|
|
113
|
+
if (file === "pi-orchestrator.md") continue;
|
|
114
|
+
const fullPath = resolve(piPiDir, file);
|
|
115
|
+
const def = parseAgentFile(fullPath);
|
|
116
|
+
if (def) {
|
|
117
|
+
const key = def.name.toLowerCase();
|
|
118
|
+
if (!experts.has(key)) {
|
|
119
|
+
experts.set(key, {
|
|
120
|
+
def,
|
|
121
|
+
status: "idle",
|
|
122
|
+
question: "",
|
|
123
|
+
elapsed: 0,
|
|
124
|
+
lastLine: "",
|
|
125
|
+
queryCount: 0,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Grid Rendering ───────────────────────────
|
|
134
|
+
|
|
135
|
+
function renderCard(state: ExpertState, colWidth: number, theme: any): string[] {
|
|
136
|
+
const w = colWidth - 2;
|
|
137
|
+
const truncate = (s: string, max: number) => s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
138
|
+
|
|
139
|
+
const statusColor = state.status === "idle" ? "dim"
|
|
140
|
+
: state.status === "researching" ? "accent"
|
|
141
|
+
: state.status === "done" ? "success" : "error";
|
|
142
|
+
const statusIcon = state.status === "idle" ? "○"
|
|
143
|
+
: state.status === "researching" ? "◉"
|
|
144
|
+
: state.status === "done" ? "✓" : "✗";
|
|
145
|
+
|
|
146
|
+
const name = displayName(state.def.name);
|
|
147
|
+
const nameStr = theme.fg("accent", theme.bold(truncate(name, w)));
|
|
148
|
+
const nameVisible = Math.min(name.length, w);
|
|
149
|
+
|
|
150
|
+
const statusStr = `${statusIcon} ${state.status}`;
|
|
151
|
+
const timeStr = state.status !== "idle" ? ` ${Math.round(state.elapsed / 1000)}s` : "";
|
|
152
|
+
const queriesStr = state.queryCount > 0 ? ` (${state.queryCount})` : "";
|
|
153
|
+
const statusLine = theme.fg(statusColor, statusStr + timeStr + queriesStr);
|
|
154
|
+
const statusVisible = statusStr.length + timeStr.length + queriesStr.length;
|
|
155
|
+
|
|
156
|
+
const workRaw = state.question || state.def.description;
|
|
157
|
+
const workText = truncate(workRaw, Math.min(50, w - 1));
|
|
158
|
+
const workLine = theme.fg("muted", workText);
|
|
159
|
+
const workVisible = workText.length;
|
|
160
|
+
|
|
161
|
+
const lastRaw = state.lastLine || "";
|
|
162
|
+
const lastText = truncate(lastRaw, Math.min(50, w - 1));
|
|
163
|
+
const lastLineRendered = lastText ? theme.fg("dim", lastText) : theme.fg("dim", "—");
|
|
164
|
+
const lastVisible = lastText ? lastText.length : 1;
|
|
165
|
+
|
|
166
|
+
const colors = EXPERT_COLORS[state.def.name];
|
|
167
|
+
const bg = colors?.bg ?? "";
|
|
168
|
+
const br = colors?.br ?? "";
|
|
169
|
+
const bgr = bg ? BG_RESET : "";
|
|
170
|
+
const fgr = br ? FG_RESET : "";
|
|
171
|
+
|
|
172
|
+
// br colors the box-drawing characters; bg fills behind them so the
|
|
173
|
+
// full card — top line, side bars, bottom line — is one solid block.
|
|
174
|
+
const bord = (s: string) => bg + br + s + bgr + fgr;
|
|
175
|
+
|
|
176
|
+
const top = "┌" + "─".repeat(w) + "┐";
|
|
177
|
+
const bot = "└" + "─".repeat(w) + "┘";
|
|
178
|
+
|
|
179
|
+
// bg fills the inner content area; re-applied before padding to ensure
|
|
180
|
+
// the full row is colored even if theme.fg uses a full ANSI reset inside.
|
|
181
|
+
const border = (content: string, visLen: number) => {
|
|
182
|
+
const pad = " ".repeat(Math.max(0, w - visLen));
|
|
183
|
+
return bord("│") + bg + content + bg + pad + bgr + bord("│");
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return [
|
|
187
|
+
bord(top),
|
|
188
|
+
border(" " + nameStr, 1 + nameVisible),
|
|
189
|
+
border(" " + statusLine, 1 + statusVisible),
|
|
190
|
+
border(" " + workLine, 1 + workVisible),
|
|
191
|
+
border(" " + lastLineRendered, 1 + lastVisible),
|
|
192
|
+
bord(bot),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function updateWidget() {
|
|
197
|
+
if (!widgetCtx) return;
|
|
198
|
+
|
|
199
|
+
widgetCtx.ui.setWidget("pi-pi-grid", (_tui: any, theme: any) => {
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
render(width: number): string[] {
|
|
203
|
+
if (experts.size === 0) {
|
|
204
|
+
return ["", theme.fg("dim", " No experts found. Add agent .md files to .pi/agents/pi-pi/")];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const cols = Math.min(gridCols, experts.size);
|
|
208
|
+
const gap = 1;
|
|
209
|
+
// avoid Text component's ANSI-width miscounting by returning raw lines
|
|
210
|
+
const colWidth = Math.floor((width - gap * (cols - 1)) / cols) - 1;
|
|
211
|
+
const allExperts = Array.from(experts.values());
|
|
212
|
+
|
|
213
|
+
const lines: string[] = [""]; // top margin
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < allExperts.length; i += cols) {
|
|
216
|
+
const rowExperts = allExperts.slice(i, i + cols);
|
|
217
|
+
const cards = rowExperts.map(e => renderCard(e, colWidth, theme));
|
|
218
|
+
|
|
219
|
+
while (cards.length < cols) {
|
|
220
|
+
cards.push(Array(6).fill(" ".repeat(colWidth)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const cardHeight = cards[0].length;
|
|
224
|
+
for (let line = 0; line < cardHeight; line++) {
|
|
225
|
+
lines.push(cards.map(card => card[line] || "").join(" ".repeat(gap)));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return lines;
|
|
230
|
+
},
|
|
231
|
+
invalidate() {},
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Query Expert ─────────────────────────────
|
|
237
|
+
|
|
238
|
+
function queryExpert(
|
|
239
|
+
expertName: string,
|
|
240
|
+
question: string,
|
|
241
|
+
ctx: any,
|
|
242
|
+
): Promise<{ output: string; exitCode: number; elapsed: number }> {
|
|
243
|
+
const key = expertName.toLowerCase();
|
|
244
|
+
const state = experts.get(key);
|
|
245
|
+
if (!state) {
|
|
246
|
+
return Promise.resolve({
|
|
247
|
+
output: `Expert "${expertName}" not found. Available: ${Array.from(experts.values()).map(s => s.def.name).join(", ")}`,
|
|
248
|
+
exitCode: 1,
|
|
249
|
+
elapsed: 0,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (state.status === "researching") {
|
|
254
|
+
return Promise.resolve({
|
|
255
|
+
output: `Expert "${displayName(state.def.name)}" is already researching. Wait for it to finish.`,
|
|
256
|
+
exitCode: 1,
|
|
257
|
+
elapsed: 0,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
state.status = "researching";
|
|
262
|
+
state.question = question;
|
|
263
|
+
state.elapsed = 0;
|
|
264
|
+
state.lastLine = "";
|
|
265
|
+
state.queryCount++;
|
|
266
|
+
updateWidget();
|
|
267
|
+
|
|
268
|
+
const startTime = Date.now();
|
|
269
|
+
state.timer = setInterval(() => {
|
|
270
|
+
state.elapsed = Date.now() - startTime;
|
|
271
|
+
updateWidget();
|
|
272
|
+
}, 1000);
|
|
273
|
+
|
|
274
|
+
const model = ctx.model
|
|
275
|
+
? `${ctx.model.provider}/${ctx.model.id}`
|
|
276
|
+
: "openrouter/google/gemini-3-flash-preview";
|
|
277
|
+
|
|
278
|
+
const args = [
|
|
279
|
+
"--mode", "json",
|
|
280
|
+
"-p",
|
|
281
|
+
"--no-session",
|
|
282
|
+
"--no-extensions",
|
|
283
|
+
"--model", model,
|
|
284
|
+
"--tools", state.def.tools,
|
|
285
|
+
"--thinking", "off",
|
|
286
|
+
"--append-system-prompt", state.def.systemPrompt,
|
|
287
|
+
question,
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const textChunks: string[] = [];
|
|
291
|
+
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
const proc = spawn("pi", args, {
|
|
294
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
295
|
+
env: { ...process.env },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
let buffer = "";
|
|
299
|
+
|
|
300
|
+
proc.stdout!.setEncoding("utf-8");
|
|
301
|
+
proc.stdout!.on("data", (chunk: string) => {
|
|
302
|
+
buffer += chunk;
|
|
303
|
+
const lines = buffer.split("\n");
|
|
304
|
+
buffer = lines.pop() || "";
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
if (!line.trim()) continue;
|
|
307
|
+
try {
|
|
308
|
+
const event = JSON.parse(line);
|
|
309
|
+
if (event.type === "message_update") {
|
|
310
|
+
const delta = event.assistantMessageEvent;
|
|
311
|
+
if (delta?.type === "text_delta") {
|
|
312
|
+
textChunks.push(delta.delta || "");
|
|
313
|
+
const full = textChunks.join("");
|
|
314
|
+
const last = full.split("\n").filter((l: string) => l.trim()).pop() || "";
|
|
315
|
+
state.lastLine = last;
|
|
316
|
+
updateWidget();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch {}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
proc.stderr!.setEncoding("utf-8");
|
|
324
|
+
proc.stderr!.on("data", () => {});
|
|
325
|
+
|
|
326
|
+
proc.on("close", (code) => {
|
|
327
|
+
if (buffer.trim()) {
|
|
328
|
+
try {
|
|
329
|
+
const event = JSON.parse(buffer);
|
|
330
|
+
if (event.type === "message_update") {
|
|
331
|
+
const delta = event.assistantMessageEvent;
|
|
332
|
+
if (delta?.type === "text_delta") textChunks.push(delta.delta || "");
|
|
333
|
+
}
|
|
334
|
+
} catch {}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
clearInterval(state.timer);
|
|
338
|
+
state.elapsed = Date.now() - startTime;
|
|
339
|
+
state.status = code === 0 ? "done" : "error";
|
|
340
|
+
|
|
341
|
+
const full = textChunks.join("");
|
|
342
|
+
state.lastLine = full.split("\n").filter((l: string) => l.trim()).pop() || "";
|
|
343
|
+
updateWidget();
|
|
344
|
+
|
|
345
|
+
ctx.ui.notify(
|
|
346
|
+
`${displayName(state.def.name)} ${state.status} in ${Math.round(state.elapsed / 1000)}s`,
|
|
347
|
+
state.status === "done" ? "success" : "error"
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
resolve({
|
|
351
|
+
output: full,
|
|
352
|
+
exitCode: code ?? 1,
|
|
353
|
+
elapsed: state.elapsed,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
proc.on("error", (err) => {
|
|
358
|
+
clearInterval(state.timer);
|
|
359
|
+
state.status = "error";
|
|
360
|
+
state.lastLine = `Error: ${err.message}`;
|
|
361
|
+
updateWidget();
|
|
362
|
+
resolve({
|
|
363
|
+
output: `Error spawning expert: ${err.message}`,
|
|
364
|
+
exitCode: 1,
|
|
365
|
+
elapsed: Date.now() - startTime,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── query_experts Tool (parallel) ───────────
|
|
372
|
+
|
|
373
|
+
pi.registerTool({
|
|
374
|
+
name: "query_experts",
|
|
375
|
+
label: "Query Experts",
|
|
376
|
+
description: `Query one or more Pi domain experts IN PARALLEL. All experts run simultaneously as concurrent subprocesses.
|
|
377
|
+
|
|
378
|
+
Pass an array of queries — each with an expert name and a specific question. All experts start at the same time and their results are returned together.
|
|
379
|
+
|
|
380
|
+
Available experts:
|
|
381
|
+
- ext-expert: Extensions — tools, events, commands, rendering, state management
|
|
382
|
+
- theme-expert: Themes — JSON format, 51 color tokens, vars, color values
|
|
383
|
+
- skill-expert: Skills — SKILL.md multi-file packages, scripts, references, frontmatter
|
|
384
|
+
- config-expert: Settings — settings.json, providers, models, packages, keybindings
|
|
385
|
+
- tui-expert: TUI — components, keyboard input, overlays, widgets, footers, editors
|
|
386
|
+
- prompt-expert: Prompt templates — single-file .md commands, arguments ($1, $@)
|
|
387
|
+
- agent-expert: Agent definitions — .md personas, tools, teams.yaml, orchestration
|
|
388
|
+
- keybinding-expert: Keyboard shortcuts — registerShortcut(), Key IDs, reserved keys, macOS terminal compatibility
|
|
389
|
+
|
|
390
|
+
Ask specific questions about what you need to BUILD. Each expert will return documentation excerpts, code patterns, and implementation guidance.`,
|
|
391
|
+
|
|
392
|
+
parameters: Type.Object({
|
|
393
|
+
queries: Type.Array(
|
|
394
|
+
Type.Object({
|
|
395
|
+
expert: Type.String({
|
|
396
|
+
description: "Expert name: ext-expert, theme-expert, skill-expert, config-expert, tui-expert, prompt-expert, or agent-expert",
|
|
397
|
+
}),
|
|
398
|
+
question: Type.String({
|
|
399
|
+
description: "Specific question about what you need to build. Include context about the target component.",
|
|
400
|
+
}),
|
|
401
|
+
}),
|
|
402
|
+
{ description: "Array of expert queries to run in parallel" },
|
|
403
|
+
),
|
|
404
|
+
}),
|
|
405
|
+
|
|
406
|
+
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
407
|
+
const { queries } = params as { queries: { expert: string; question: string }[] };
|
|
408
|
+
|
|
409
|
+
if (!queries || queries.length === 0) {
|
|
410
|
+
return {
|
|
411
|
+
content: [{ type: "text", text: "No queries provided." }],
|
|
412
|
+
details: { results: [], status: "error" },
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const names = queries.map(q => displayName(q.expert)).join(", ");
|
|
417
|
+
if (onUpdate) {
|
|
418
|
+
onUpdate({
|
|
419
|
+
content: [{ type: "text", text: `Querying ${queries.length} experts in parallel: ${names}` }],
|
|
420
|
+
details: { queries, status: "researching", results: [] },
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Launch ALL experts concurrently — allSettled so one failure
|
|
425
|
+
// never discards results from the others
|
|
426
|
+
const settled = await Promise.allSettled(
|
|
427
|
+
queries.map(async ({ expert, question }) => {
|
|
428
|
+
const result = await queryExpert(expert, question, ctx);
|
|
429
|
+
const truncated = result.output.length > 12000
|
|
430
|
+
? result.output.slice(0, 12000) + "\n\n... [truncated — ask follow-up for more]"
|
|
431
|
+
: result.output;
|
|
432
|
+
const status = result.exitCode === 0 ? "done" : "error";
|
|
433
|
+
return {
|
|
434
|
+
expert,
|
|
435
|
+
question,
|
|
436
|
+
status,
|
|
437
|
+
elapsed: result.elapsed,
|
|
438
|
+
exitCode: result.exitCode,
|
|
439
|
+
output: truncated,
|
|
440
|
+
fullOutput: result.output,
|
|
441
|
+
};
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const results = settled.map((s, i) =>
|
|
446
|
+
s.status === "fulfilled"
|
|
447
|
+
? s.value
|
|
448
|
+
: {
|
|
449
|
+
expert: queries[i].expert,
|
|
450
|
+
question: queries[i].question,
|
|
451
|
+
status: "error" as const,
|
|
452
|
+
elapsed: 0,
|
|
453
|
+
exitCode: 1,
|
|
454
|
+
output: `Error: ${(s.reason as any)?.message || s.reason}`,
|
|
455
|
+
fullOutput: "",
|
|
456
|
+
},
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// Build combined response
|
|
460
|
+
const sections = results.map(r => {
|
|
461
|
+
const icon = r.status === "done" ? "✓" : "✗";
|
|
462
|
+
return `## [${icon}] ${displayName(r.expert)} (${Math.round(r.elapsed / 1000)}s)\n\n${r.output}`;
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: sections.join("\n\n---\n\n") }],
|
|
467
|
+
details: {
|
|
468
|
+
results,
|
|
469
|
+
status: results.every(r => r.status === "done") ? "done" : "partial",
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
renderCall(args, theme) {
|
|
475
|
+
const queries = (args as any).queries || [];
|
|
476
|
+
const names = queries.map((q: any) => displayName(q.expert || "?")).join(", ");
|
|
477
|
+
return new Text(
|
|
478
|
+
theme.fg("toolTitle", theme.bold("query_experts ")) +
|
|
479
|
+
theme.fg("accent", `${queries.length} parallel`) +
|
|
480
|
+
theme.fg("dim", " — ") +
|
|
481
|
+
theme.fg("muted", names),
|
|
482
|
+
0, 0,
|
|
483
|
+
);
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
renderResult(result, options, theme) {
|
|
487
|
+
const details = result.details as any;
|
|
488
|
+
if (!details?.results) {
|
|
489
|
+
const text = result.content[0];
|
|
490
|
+
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (options.isPartial || details.status === "researching") {
|
|
494
|
+
const count = details.queries?.length || "?";
|
|
495
|
+
return new Text(
|
|
496
|
+
theme.fg("accent", `◉ ${count} experts`) +
|
|
497
|
+
theme.fg("dim", " researching in parallel..."),
|
|
498
|
+
0, 0,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const lines = (details.results as any[]).map((r: any) => {
|
|
503
|
+
const icon = r.status === "done" ? "✓" : "✗";
|
|
504
|
+
const color = r.status === "done" ? "success" : "error";
|
|
505
|
+
const elapsed = typeof r.elapsed === "number" ? Math.round(r.elapsed / 1000) : 0;
|
|
506
|
+
return theme.fg(color, `${icon} ${displayName(r.expert)}`) +
|
|
507
|
+
theme.fg("dim", ` ${elapsed}s`);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const header = lines.join(theme.fg("dim", " · "));
|
|
511
|
+
|
|
512
|
+
if (options.expanded && details.results) {
|
|
513
|
+
const expanded = (details.results as any[]).map((r: any) => {
|
|
514
|
+
const output = r.fullOutput
|
|
515
|
+
? (r.fullOutput.length > 4000 ? r.fullOutput.slice(0, 4000) + "\n... [truncated]" : r.fullOutput)
|
|
516
|
+
: r.output || "";
|
|
517
|
+
return theme.fg("accent", `── ${displayName(r.expert)} ──`) + "\n" + theme.fg("muted", output);
|
|
518
|
+
});
|
|
519
|
+
return new Text(header + "\n\n" + expanded.join("\n\n"), 0, 0);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return new Text(header, 0, 0);
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// ── Commands ─────────────────────────────────
|
|
527
|
+
|
|
528
|
+
pi.registerCommand("experts", {
|
|
529
|
+
description: "List available Pi Pi experts and their status",
|
|
530
|
+
handler: async (_args, _ctx) => {
|
|
531
|
+
widgetCtx = _ctx;
|
|
532
|
+
const lines = Array.from(experts.values())
|
|
533
|
+
.map(s => `${displayName(s.def.name)} (${s.status}, queries: ${s.queryCount}): ${s.def.description}`)
|
|
534
|
+
.join("\n");
|
|
535
|
+
_ctx.ui.notify(lines || "No experts loaded", "info");
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
pi.registerCommand("experts-grid", {
|
|
540
|
+
description: "Set expert grid columns: /experts-grid <1-5>",
|
|
541
|
+
handler: async (args, _ctx) => {
|
|
542
|
+
widgetCtx = _ctx;
|
|
543
|
+
const n = parseInt(args?.trim() || "", 10);
|
|
544
|
+
if (n >= 1 && n <= 5) {
|
|
545
|
+
gridCols = n;
|
|
546
|
+
_ctx.ui.notify(`Grid set to ${gridCols} columns`, "info");
|
|
547
|
+
updateWidget();
|
|
548
|
+
} else {
|
|
549
|
+
_ctx.ui.notify("Usage: /experts-grid <1-5>", "error");
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// ── System Prompt ────────────────────────────
|
|
555
|
+
|
|
556
|
+
pi.on("before_agent_start", async (_event, _ctx) => {
|
|
557
|
+
const expertCatalog = Array.from(experts.values())
|
|
558
|
+
.map(s => `### ${displayName(s.def.name)}\n**Query as:** \`${s.def.name}\`\n${s.def.description}`)
|
|
559
|
+
.join("\n\n");
|
|
560
|
+
|
|
561
|
+
const expertNames = Array.from(experts.values()).map(s => displayName(s.def.name)).join(", ");
|
|
562
|
+
|
|
563
|
+
const orchestratorPath = join(_ctx.cwd, ".pi", "agents", "pi-pi", "pi-orchestrator.md");
|
|
564
|
+
let systemPrompt = "";
|
|
565
|
+
try {
|
|
566
|
+
const raw = readFileSync(orchestratorPath, "utf-8");
|
|
567
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
568
|
+
const template = match ? match[2].trim() : raw;
|
|
569
|
+
|
|
570
|
+
systemPrompt = template
|
|
571
|
+
.replace("{{EXPERT_COUNT}}", experts.size.toString())
|
|
572
|
+
.replace("{{EXPERT_NAMES}}", expertNames)
|
|
573
|
+
.replace("{{EXPERT_CATALOG}}", expertCatalog);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
systemPrompt = "Error: Could not load pi-orchestrator.md. Make sure it exists in .pi/agents/pi-pi/.";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { systemPrompt };
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// ── Session Start ────────────────────────────
|
|
582
|
+
|
|
583
|
+
pi.on("session_start", async (_event, _ctx) => {
|
|
584
|
+
if (widgetCtx) {
|
|
585
|
+
widgetCtx.ui.setWidget("pi-pi-grid", undefined);
|
|
586
|
+
}
|
|
587
|
+
widgetCtx = _ctx;
|
|
588
|
+
|
|
589
|
+
loadExperts(_ctx.cwd);
|
|
590
|
+
updateWidget();
|
|
591
|
+
|
|
592
|
+
const expertNames = Array.from(experts.values()).map(s => displayName(s.def.name)).join(", ");
|
|
593
|
+
_ctx.ui.setStatus("pi-pi", `Pi Pi (${experts.size} experts)`);
|
|
594
|
+
_ctx.ui.notify(
|
|
595
|
+
`Pi Pi loaded — ${experts.size} experts: ${expertNames}\n\n` +
|
|
596
|
+
`/experts List experts and status\n` +
|
|
597
|
+
`/experts-grid N Set grid columns (1-5)\n\n` +
|
|
598
|
+
`Ask me to build any Pi agent component!`,
|
|
599
|
+
"info",
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// Custom footer
|
|
603
|
+
_ctx.ui.setFooter((_tui, theme, _footerData) => ({
|
|
604
|
+
dispose: () => {},
|
|
605
|
+
invalidate() {},
|
|
606
|
+
render(width: number): string[] {
|
|
607
|
+
const model = _ctx.model?.id || "no-model";
|
|
608
|
+
const usage = _ctx.getContextUsage();
|
|
609
|
+
const pct = usage ? usage.percent : 0;
|
|
610
|
+
const filled = Math.round(pct / 10);
|
|
611
|
+
const bar = "#".repeat(filled) + "-".repeat(10 - filled);
|
|
612
|
+
|
|
613
|
+
const active = Array.from(experts.values()).filter(e => e.status === "researching").length;
|
|
614
|
+
const done = Array.from(experts.values()).filter(e => e.status === "done").length;
|
|
615
|
+
|
|
616
|
+
const left = theme.fg("dim", ` ${model}`) +
|
|
617
|
+
theme.fg("muted", " · ") +
|
|
618
|
+
theme.fg("accent", "Pi Pi");
|
|
619
|
+
const mid = active > 0
|
|
620
|
+
? theme.fg("accent", ` ◉ ${active} researching`)
|
|
621
|
+
: done > 0
|
|
622
|
+
? theme.fg("success", ` ✓ ${done} done`)
|
|
623
|
+
: "";
|
|
624
|
+
const right = theme.fg("dim", `[${bar}] ${Math.round(pct)}% `);
|
|
625
|
+
const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(mid) - visibleWidth(right)));
|
|
626
|
+
|
|
627
|
+
return [truncateToWidth(left + mid + pad + right, width)];
|
|
628
|
+
},
|
|
629
|
+
}));
|
|
630
|
+
});
|
|
631
|
+
}
|