oh-pi 0.1.75 → 0.1.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +25 -12
- package/README.md +35 -12
- package/README.zh.md +41 -12
- package/package.json +1 -1
- package/pi-package/extensions/ant-colony/index.ts +200 -28
- package/pi-package/extensions/ant-colony/ui.test.ts +18 -1
- package/pi-package/extensions/ant-colony/ui.ts +32 -2
package/README.fr.md
CHANGED
|
@@ -22,20 +22,28 @@ npx oh-pi
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
pi-coding-agent est puissant dès l'installation. Mais configurer manuellement les fournisseurs, thèmes, extensions, compétences et modèles de prompts est fastidieux. oh-pi offre une TUI moderne qui fait tout en moins d'une minute — et embarque un **essaim de fourmis** qui transforme pi en système multi-agents.
|
|
28
|
-
|
|
29
|
-
## Démarrage rapide
|
|
25
|
+
## Démarrage en 30 secondes
|
|
30
26
|
|
|
31
27
|
```bash
|
|
32
28
|
npx oh-pi # tout configurer
|
|
33
29
|
pi # commencer à coder
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
oh-pi détecte automatiquement votre environnement, vous guide dans une TUI moderne, puis écrit `~/.pi/agent/`.
|
|
33
|
+
|
|
34
|
+
Déjà configuré ? oh-pi détecte les fichiers existants et propose une **sauvegarde avant écrasement**.
|
|
35
|
+
|
|
36
|
+
## Valeur en 2 minutes
|
|
37
|
+
|
|
38
|
+
pi-coding-agent est puissant par défaut, mais la configuration manuelle (fournisseurs, thèmes, extensions, skills, prompts) prend du temps. oh-pi compresse cette phase en moins d'une minute — puis ajoute la colonie pour les tâches complexes.
|
|
39
|
+
|
|
40
|
+
- [`docs/DEMO-SCRIPT.md`](./docs/DEMO-SCRIPT.md) — démo rapide en 2 minutes
|
|
41
|
+
- [`ROADMAP.md`](./ROADMAP.md) — positionnement, jalons, métriques
|
|
42
|
+
- [`DECISIONS.md`](./DECISIONS.md) — décisions produit et compromis techniques
|
|
43
|
+
|
|
44
|
+
## Quand ne pas utiliser la colonie
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
Utilisez le flux pi classique (sans colonie) si la tâche est petite, très exploratoire, ou nécessite un pilotage humain continu.
|
|
39
47
|
|
|
40
48
|
## Ce que vous obtenez
|
|
41
49
|
|
|
@@ -139,14 +147,19 @@ Utilisez `/colony-stop` pour arrêter une colonie en cours.
|
|
|
139
147
|
|
|
140
148
|
### Protocole de signaux
|
|
141
149
|
|
|
142
|
-
La colonie communique
|
|
150
|
+
La colonie communique via des signaux structurés, pour éviter toute supposition côté modèle :
|
|
143
151
|
|
|
144
152
|
| Signal | Signification |
|
|
145
153
|
|--------|---------------|
|
|
146
|
-
| `COLONY_SIGNAL:LAUNCHED` | Colonie démarrée
|
|
147
|
-
| `COLONY_SIGNAL:
|
|
148
|
-
| `COLONY_SIGNAL:
|
|
149
|
-
| `COLONY_SIGNAL:
|
|
154
|
+
| `COLONY_SIGNAL:LAUNCHED` | Colonie démarrée en arrière-plan |
|
|
155
|
+
| `COLONY_SIGNAL:SCOUTING` | Vague d'éclaireuses en exploration/planification |
|
|
156
|
+
| `COLONY_SIGNAL:PLANNING_RECOVERY` | Boucle de récupération du plan en cours |
|
|
157
|
+
| `COLONY_SIGNAL:WORKING` | Exécution des tâches par les ouvrières |
|
|
158
|
+
| `COLONY_SIGNAL:REVIEWING` | Revue qualité par les soldats |
|
|
159
|
+
| `COLONY_SIGNAL:TASK_DONE` | Tâche terminée (jalon de progression) |
|
|
160
|
+
| `COLONY_SIGNAL:COMPLETE` | Mission terminée, rapport injecté |
|
|
161
|
+
| `COLONY_SIGNAL:FAILED` | Mission échouée avec diagnostic |
|
|
162
|
+
| `COLONY_SIGNAL:BUDGET_EXCEEDED` | Budget maximal atteint |
|
|
150
163
|
|
|
151
164
|
### Contrôle des tours
|
|
152
165
|
|
package/README.md
CHANGED
|
@@ -22,20 +22,28 @@ npx oh-pi
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
pi-coding-agent is powerful out of the box. But configuring providers, themes, extensions, skills, and prompts by hand is tedious. oh-pi gives you a modern TUI that does it all in under a minute — and ships an **ant colony swarm** that turns pi into a multi-agent system.
|
|
28
|
-
|
|
29
|
-
## Quick Start
|
|
25
|
+
## 30-Second Start
|
|
30
26
|
|
|
31
27
|
```bash
|
|
32
28
|
npx oh-pi # configure everything
|
|
33
29
|
pi # start coding
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
oh-pi auto-detects your environment, guides setup in a modern TUI, and writes `~/.pi/agent/` for you.
|
|
33
|
+
|
|
34
|
+
Already configured? It detects existing files and offers **backup before overwriting**.
|
|
35
|
+
|
|
36
|
+
## 2-Minute Value
|
|
37
|
+
|
|
38
|
+
pi-coding-agent is powerful by default, but manual setup across providers, themes, extensions, skills, and prompts is slow. oh-pi compresses that setup into under a minute — then adds an **ant colony swarm** for multi-agent execution.
|
|
39
|
+
|
|
40
|
+
Want the fast walkthrough? See [`docs/DEMO-SCRIPT.md`](./docs/DEMO-SCRIPT.md).
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
## When Not to Use Ant Colony
|
|
43
|
+
|
|
44
|
+
Use plain pi workflows (without colony) when your task is tiny, highly exploratory, or needs constant human steering.
|
|
45
|
+
|
|
46
|
+
For positioning, scope, and milestones, see [`ROADMAP.md`](./ROADMAP.md). For rationale behind key trade-offs, see [`DECISIONS.md`](./DECISIONS.md).
|
|
39
47
|
|
|
40
48
|
## What You Get
|
|
41
49
|
|
|
@@ -96,6 +104,16 @@ oh-pi:
|
|
|
96
104
|
✅ Done — report auto-injected into conversation
|
|
97
105
|
```
|
|
98
106
|
|
|
107
|
+
### What's new in v0.1.75
|
|
108
|
+
|
|
109
|
+
- **Planning Recovery Loop**: if scouts return unstructured intel, colony enters `planning_recovery` instead of failing immediately.
|
|
110
|
+
- **Plan Validation Gate**: before workers start, tasks are validated (title/description/caste/priority).
|
|
111
|
+
- **Scout Quorum for complex goals**: multi-step goals default to at least 2 scouts for better planning reliability.
|
|
112
|
+
|
|
113
|
+
### Colony lifecycle (simple)
|
|
114
|
+
|
|
115
|
+
`SCOUTING → (if needed) PLANNING_RECOVERY → WORKING → REVIEWING → DONE`
|
|
116
|
+
|
|
99
117
|
### Architecture
|
|
100
118
|
|
|
101
119
|
Each ant is an in-process `AgentSession` (pi SDK), not a child process:
|
|
@@ -139,14 +157,19 @@ Use `/colony-stop` to abort a running colony.
|
|
|
139
157
|
|
|
140
158
|
### Signal Protocol
|
|
141
159
|
|
|
142
|
-
The colony communicates with the main conversation via structured signals,
|
|
160
|
+
The colony communicates with the main conversation via structured signals, so the model never has to guess background state:
|
|
143
161
|
|
|
144
162
|
| Signal | Meaning |
|
|
145
163
|
|--------|---------|
|
|
146
|
-
| `COLONY_SIGNAL:LAUNCHED` | Colony started
|
|
147
|
-
| `COLONY_SIGNAL:
|
|
148
|
-
| `COLONY_SIGNAL:
|
|
149
|
-
| `COLONY_SIGNAL:
|
|
164
|
+
| `COLONY_SIGNAL:LAUNCHED` | Colony started in background |
|
|
165
|
+
| `COLONY_SIGNAL:SCOUTING` | Scout wave is exploring / planning |
|
|
166
|
+
| `COLONY_SIGNAL:PLANNING_RECOVERY` | Plan recovery loop is restructuring tasks |
|
|
167
|
+
| `COLONY_SIGNAL:WORKING` | Worker phase is executing tasks |
|
|
168
|
+
| `COLONY_SIGNAL:REVIEWING` | Soldier review phase is active |
|
|
169
|
+
| `COLONY_SIGNAL:TASK_DONE` | A task finished (progress checkpoint) |
|
|
170
|
+
| `COLONY_SIGNAL:COMPLETE` | Colony finished and report injected |
|
|
171
|
+
| `COLONY_SIGNAL:FAILED` | Colony failed with diagnostics |
|
|
172
|
+
| `COLONY_SIGNAL:BUDGET_EXCEEDED` | Budget limit reached |
|
|
150
173
|
|
|
151
174
|
### Turn Control
|
|
152
175
|
|
package/README.zh.md
CHANGED
|
@@ -22,20 +22,34 @@ npx oh-pi
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
pi-coding-agent 开箱即用很强大,但手动配置提供商、主题、扩展、技能和提示词模板很繁琐。oh-pi 提供现代化 TUI,一分钟内搞定一切 —— 还附带一个**蚁群系统**,把 pi 变成多智能体平台。
|
|
28
|
-
|
|
29
|
-
## 快速开始
|
|
25
|
+
## 30 秒上手
|
|
30
26
|
|
|
31
27
|
```bash
|
|
32
28
|
npx oh-pi # 配置一切
|
|
33
29
|
pi # 开始编码
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
就这样。oh-pi
|
|
32
|
+
就这样。oh-pi 会自动检测环境、引导配置,并写入 `~/.pi/agent/`。
|
|
33
|
+
|
|
34
|
+
已有配置?会先备份,再覆盖。
|
|
35
|
+
|
|
36
|
+
## 2 分钟看懂价值
|
|
37
|
+
|
|
38
|
+
oh-pi 把原本分散且手工的配置流程(提供商、主题、扩展、技能、提示词模板)整合成一次引导,通常 1 分钟内完成。
|
|
39
|
+
|
|
40
|
+
当任务涉及多文件或可并行流程时,可启用蚁群系统,把 pi 升级为可协作的多智能体执行流。
|
|
41
|
+
|
|
42
|
+
- [`docs/DEMO-SCRIPT.zh.md`](./docs/DEMO-SCRIPT.zh.md) — 2 分钟演示脚本(价值与节奏)
|
|
43
|
+
- [`ROADMAP.md`](./ROADMAP.md) — 定位、里程碑与衡量指标
|
|
44
|
+
- [`DECISIONS.md`](./DECISIONS.md) — 阶段性关键决策与取舍依据
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
## 何时不该用蚁群
|
|
47
|
+
|
|
48
|
+
以下场景建议**不要**启用蚁群,直接单代理更快:
|
|
49
|
+
|
|
50
|
+
- 只改 1 个文件、改动范围明确
|
|
51
|
+
- 快速问答、解释代码、一次性小修复
|
|
52
|
+
- 你需要严格串行控制每一步修改
|
|
39
53
|
|
|
40
54
|
## 你会得到
|
|
41
55
|
|
|
@@ -96,6 +110,16 @@ oh-pi:
|
|
|
96
110
|
✅ 完成 — 报告自动注入对话
|
|
97
111
|
```
|
|
98
112
|
|
|
113
|
+
### v0.1.75 更新亮点
|
|
114
|
+
|
|
115
|
+
- **Planning Recovery 回路**:当 scout 产出非结构化情报时,进入 `planning_recovery`,而不是直接失败。
|
|
116
|
+
- **执行前计划校验**:worker 启动前会校验任务字段完整性(title/description/caste/priority)。
|
|
117
|
+
- **复杂目标 Scout Quorum**:多步骤目标默认至少 2 个 scout,提升规划可靠性。
|
|
118
|
+
|
|
119
|
+
### 生命周期(简化)
|
|
120
|
+
|
|
121
|
+
`SCOUTING →(必要时)PLANNING_RECOVERY → WORKING → REVIEWING → DONE`
|
|
122
|
+
|
|
99
123
|
### 架构
|
|
100
124
|
|
|
101
125
|
每只蚂蚁是进程内的 `AgentSession`(pi SDK),而非子进程:
|
|
@@ -139,14 +163,19 @@ pi(主进程)
|
|
|
139
163
|
|
|
140
164
|
### 信号协议
|
|
141
165
|
|
|
142
|
-
|
|
166
|
+
蚁群通过结构化信号与主对话通信,让模型无需猜测后台状态:
|
|
143
167
|
|
|
144
168
|
| 信号 | 含义 |
|
|
145
169
|
|------|------|
|
|
146
|
-
| `COLONY_SIGNAL:LAUNCHED` |
|
|
147
|
-
| `COLONY_SIGNAL:
|
|
148
|
-
| `COLONY_SIGNAL:
|
|
149
|
-
| `COLONY_SIGNAL:
|
|
170
|
+
| `COLONY_SIGNAL:LAUNCHED` | 蚁群已在后台启动 |
|
|
171
|
+
| `COLONY_SIGNAL:SCOUTING` | 侦察波次正在探索/规划 |
|
|
172
|
+
| `COLONY_SIGNAL:PLANNING_RECOVERY` | 计划恢复回路正在重组任务 |
|
|
173
|
+
| `COLONY_SIGNAL:WORKING` | 工蚁执行阶段进行中 |
|
|
174
|
+
| `COLONY_SIGNAL:REVIEWING` | 兵蚁审查阶段进行中 |
|
|
175
|
+
| `COLONY_SIGNAL:TASK_DONE` | 单个任务完成(进度检查点) |
|
|
176
|
+
| `COLONY_SIGNAL:COMPLETE` | 蚁群完成并注入报告 |
|
|
177
|
+
| `COLONY_SIGNAL:FAILED` | 蚁群失败并附带诊断信息 |
|
|
178
|
+
| `COLONY_SIGNAL:BUDGET_EXCEEDED` | 达到预算上限 |
|
|
150
179
|
|
|
151
180
|
### 轮次控制
|
|
152
181
|
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ import { runColony, resumeColony, type QueenCallbacks } from "./queen.js";
|
|
|
18
18
|
import { Nest } from "./nest.js";
|
|
19
19
|
import type { ColonyState, ColonyMetrics, AntStreamEvent } from "./types.js";
|
|
20
20
|
|
|
21
|
-
import { formatDuration, formatCost, formatTokens, statusIcon, casteIcon, buildReport } from "./ui.js";
|
|
21
|
+
import { formatDuration, formatCost, formatTokens, statusIcon, statusLabel, progressBar, casteIcon, buildReport } from "./ui.js";
|
|
22
22
|
|
|
23
23
|
// ═══ Background colony state ═══
|
|
24
24
|
|
|
@@ -38,12 +38,19 @@ interface AntStreamState {
|
|
|
38
38
|
tokens: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface ColonyLogEntry {
|
|
42
|
+
timestamp: number;
|
|
43
|
+
level: "info" | "warning" | "error";
|
|
44
|
+
text: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
interface BackgroundColony {
|
|
42
48
|
goal: string;
|
|
43
49
|
abortController: AbortController;
|
|
44
50
|
state: ColonyState | null;
|
|
45
51
|
phase: string;
|
|
46
52
|
antStreams: Map<string, AntStreamState>;
|
|
53
|
+
logs: ColonyLogEntry[];
|
|
47
54
|
promise: Promise<ColonyState>;
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -52,6 +59,18 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
52
59
|
// 当前运行中的后台蚁群(同时只允许一个)
|
|
53
60
|
let activeColony: BackgroundColony | null = null;
|
|
54
61
|
|
|
62
|
+
const calcProgress = (m?: ColonyMetrics | null) => {
|
|
63
|
+
if (!m || m.tasksTotal <= 0) return 0;
|
|
64
|
+
return Math.max(0, Math.min(1, m.tasksDone / m.tasksTotal));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const trim = (text: string, max: number) => text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
|
|
68
|
+
|
|
69
|
+
const pushLog = (colony: BackgroundColony, entry: Omit<ColonyLogEntry, "timestamp">) => {
|
|
70
|
+
colony.logs.push({ timestamp: Date.now(), ...entry });
|
|
71
|
+
if (colony.logs.length > 40) colony.logs.splice(0, colony.logs.length - 40);
|
|
72
|
+
};
|
|
73
|
+
|
|
55
74
|
// ─── Status 渲染 ───
|
|
56
75
|
|
|
57
76
|
let lastRender = 0;
|
|
@@ -78,10 +97,14 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
78
97
|
const { state } = activeColony;
|
|
79
98
|
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
80
99
|
const m = state?.metrics;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
100
|
+
const phase = state?.status || "scouting";
|
|
101
|
+
const progress = calcProgress(m);
|
|
102
|
+
const pct = `${Math.round(progress * 100)}%`;
|
|
103
|
+
const active = activeColony.antStreams.size;
|
|
104
|
+
|
|
105
|
+
const parts = [`🐜 ${statusIcon(phase)} ${statusLabel(phase)}`];
|
|
106
|
+
parts.push(m ? `${m.tasksDone}/${m.tasksTotal} (${pct})` : `0/0 (${pct})`);
|
|
107
|
+
parts.push(`⚡${active}`);
|
|
85
108
|
if (m) parts.push(formatCost(m.totalCost));
|
|
86
109
|
parts.push(elapsed);
|
|
87
110
|
|
|
@@ -162,9 +185,12 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
162
185
|
state: null,
|
|
163
186
|
phase: "initializing",
|
|
164
187
|
antStreams: new Map(),
|
|
188
|
+
logs: [],
|
|
165
189
|
promise: null as any, // set below
|
|
166
190
|
};
|
|
167
191
|
|
|
192
|
+
pushLog(colony, { level: "info", text: "INITIALIZING · Colony launched in background" });
|
|
193
|
+
|
|
168
194
|
let lastPhase = "";
|
|
169
195
|
|
|
170
196
|
const callbacks: QueenCallbacks = {
|
|
@@ -174,6 +200,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
174
200
|
if (signal.phase !== lastPhase) {
|
|
175
201
|
lastPhase = signal.phase;
|
|
176
202
|
const pct = Math.round(signal.progress * 100);
|
|
203
|
+
pushLog(colony, { level: "info", text: `${statusLabel(signal.phase)} ${pct}% · ${signal.message}` });
|
|
177
204
|
pi.sendMessage({
|
|
178
205
|
customType: "ant-colony-progress",
|
|
179
206
|
content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] 🐜 ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
|
|
@@ -184,6 +211,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
184
211
|
},
|
|
185
212
|
onPhase(phase, detail) {
|
|
186
213
|
colony.phase = detail;
|
|
214
|
+
pushLog(colony, { level: "info", text: `${statusLabel(phase)} · ${detail}` });
|
|
187
215
|
throttledRender();
|
|
188
216
|
},
|
|
189
217
|
onAntSpawn(ant, task) {
|
|
@@ -199,6 +227,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
199
227
|
const icon = ant.status === "done" ? "✓" : "✗";
|
|
200
228
|
const progress = m ? `${m.tasksDone}/${m.tasksTotal}` : "";
|
|
201
229
|
const cost = m ? formatCost(m.totalCost) : "";
|
|
230
|
+
pushLog(colony, {
|
|
231
|
+
level: ant.status === "done" ? "info" : "warning",
|
|
232
|
+
text: `${icon} ${task.title.slice(0, 80)} (${progress}${cost ? `, ${cost}` : ""})`,
|
|
233
|
+
});
|
|
202
234
|
pi.sendMessage({
|
|
203
235
|
customType: "ant-colony-progress",
|
|
204
236
|
content: `[COLONY_SIGNAL:TASK_DONE] 🐜 ${icon} ${task.title.slice(0, 60)} (${progress}, ${cost})`,
|
|
@@ -221,6 +253,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
221
253
|
onComplete(state) {
|
|
222
254
|
colony.state = state;
|
|
223
255
|
colony.phase = state.status === "done" ? "Colony mission complete" : "Colony failed";
|
|
256
|
+
pushLog(colony, {
|
|
257
|
+
level: state.status === "done" ? "info" : "error",
|
|
258
|
+
text: `${statusLabel(state.status)} · ${state.metrics.tasksDone}/${state.metrics.tasksTotal} · ${formatCost(state.metrics.totalCost)}`,
|
|
259
|
+
});
|
|
224
260
|
colony.antStreams.clear();
|
|
225
261
|
throttledRender();
|
|
226
262
|
},
|
|
@@ -251,6 +287,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
251
287
|
const ok = state.status === "done";
|
|
252
288
|
const report = buildReport(state);
|
|
253
289
|
const m = state.metrics;
|
|
290
|
+
pushLog(colony, {
|
|
291
|
+
level: ok ? "info" : "error",
|
|
292
|
+
text: `${ok ? "COMPLETE" : "FAILED"} · ${m.tasksDone}/${m.tasksTotal} · ${formatCost(m.totalCost)}`,
|
|
293
|
+
});
|
|
254
294
|
|
|
255
295
|
// 清理 UI
|
|
256
296
|
pi.events.emit("ant-colony:clear-ui");
|
|
@@ -268,6 +308,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
268
308
|
level: ok ? "success" : "error",
|
|
269
309
|
});
|
|
270
310
|
}).catch((e) => {
|
|
311
|
+
pushLog(colony, { level: "error", text: `CRASHED · ${String(e).slice(0, 120)}` });
|
|
271
312
|
pi.events.emit("ant-colony:clear-ui");
|
|
272
313
|
activeColony = null;
|
|
273
314
|
pi.events.emit("ant-colony:notify", { msg: `🐜 Colony crashed: ${e}`, level: "error" });
|
|
@@ -283,6 +324,29 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
283
324
|
|
|
284
325
|
|
|
285
326
|
|
|
327
|
+
// ═══ Custom message renderer for colony progress signals ═══
|
|
328
|
+
pi.registerMessageRenderer("ant-colony-progress", (message, theme) => {
|
|
329
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
330
|
+
const line = content.split("\n")[0] || content;
|
|
331
|
+
const phaseMatch = line.match(/\[COLONY_SIGNAL:([A-Z_]+)\]/);
|
|
332
|
+
const text = line.replace(/\[COLONY_SIGNAL:[A-Z_]+\]\s*/, "").trim();
|
|
333
|
+
|
|
334
|
+
const phase = phaseMatch?.[1]?.toLowerCase() || "working";
|
|
335
|
+
const icon = statusIcon(phase);
|
|
336
|
+
const label = statusLabel(phase);
|
|
337
|
+
|
|
338
|
+
const body = trim(text, 120);
|
|
339
|
+
const coloredBody = phase === "failed"
|
|
340
|
+
? theme.fg("error", body)
|
|
341
|
+
: phase === "budget_exceeded"
|
|
342
|
+
? theme.fg("warning", body)
|
|
343
|
+
: phase === "done" || phase === "complete"
|
|
344
|
+
? theme.fg("success", body)
|
|
345
|
+
: theme.fg("muted", body);
|
|
346
|
+
|
|
347
|
+
return new Text(`${icon} ${theme.fg("toolTitle", theme.bold(label))} ${coloredBody}`, 0, 0);
|
|
348
|
+
});
|
|
349
|
+
|
|
286
350
|
// ═══ Custom message renderer for colony reports ═══
|
|
287
351
|
pi.registerMessageRenderer("ant-colony-report", (message, theme) => {
|
|
288
352
|
const content = typeof message.content === "string" ? message.content : "";
|
|
@@ -304,7 +368,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
304
368
|
const taskLines = content.split("\n").filter(l => l.startsWith("- ✓") || l.startsWith("- ✗"));
|
|
305
369
|
for (const l of taskLines.slice(0, 8)) {
|
|
306
370
|
const icon = l.startsWith("- ✓") ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
307
|
-
container.addChild(new Text(` ${icon} ${theme.fg("
|
|
371
|
+
container.addChild(new Text(` ${icon} ${theme.fg("muted", l.slice(4).trim().slice(0, 70))}`, 0, 0));
|
|
308
372
|
}
|
|
309
373
|
if (taskLines.length > 8) {
|
|
310
374
|
container.addChild(new Text(theme.fg("muted", ` ⋯ +${taskLines.length - 8} more`), 0, 0));
|
|
@@ -331,6 +395,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
331
395
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
332
396
|
let cachedWidth: number | undefined;
|
|
333
397
|
let cachedLines: string[] | undefined;
|
|
398
|
+
let currentTab: "tasks" | "streams" | "log" = "tasks";
|
|
399
|
+
let taskFilter: "all" | "active" | "done" | "failed" = "all";
|
|
334
400
|
|
|
335
401
|
const buildLines = (width: number): string[] => {
|
|
336
402
|
const c = activeColony;
|
|
@@ -341,36 +407,121 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
341
407
|
|
|
342
408
|
// ── Header ──
|
|
343
409
|
const elapsed = c.state ? formatDuration(Date.now() - c.state.createdAt) : "0s";
|
|
344
|
-
const
|
|
410
|
+
const m = c.state?.metrics;
|
|
411
|
+
const phase = c.state?.status || "scouting";
|
|
412
|
+
const progress = calcProgress(m);
|
|
413
|
+
const pct = Math.round(progress * 100);
|
|
414
|
+
const cost = m ? formatCost(m.totalCost) : "$0";
|
|
415
|
+
const activeAnts = c.antStreams.size;
|
|
416
|
+
const barWidth = Math.max(10, Math.min(24, w - 28));
|
|
417
|
+
|
|
345
418
|
lines.push(theme.fg("accent", theme.bold(` 🐜 Colony Details`)) + theme.fg("muted", ` │ ${elapsed} │ ${cost}`));
|
|
346
|
-
lines.push(theme.fg("
|
|
419
|
+
lines.push(theme.fg("muted", ` Goal: ${trim(c.goal, w - 8)}`));
|
|
420
|
+
lines.push(` ${statusIcon(phase)} ${theme.bold(statusLabel(phase))} │ ${m ? `${m.tasksDone}/${m.tasksTotal}` : "0/0"} │ ${pct}% │ ⚡${activeAnts}`);
|
|
421
|
+
lines.push(theme.fg("muted", ` ${progressBar(progress, barWidth)} ${pct}%`));
|
|
422
|
+
if (c.phase && c.phase !== "initializing") {
|
|
423
|
+
lines.push(theme.fg("muted", ` Phase: ${trim(c.phase, w - 10)}`));
|
|
424
|
+
}
|
|
425
|
+
lines.push("");
|
|
426
|
+
|
|
427
|
+
// ── Tabs ──
|
|
428
|
+
const tabs: Array<{ key: "tasks" | "streams" | "log"; hotkey: string; label: string }> = [
|
|
429
|
+
{ key: "tasks", hotkey: "1", label: "Tasks" },
|
|
430
|
+
{ key: "streams", hotkey: "2", label: "Streams" },
|
|
431
|
+
{ key: "log", hotkey: "3", label: "Log" },
|
|
432
|
+
];
|
|
433
|
+
const tabLine = tabs.map((t) => {
|
|
434
|
+
const label = `[${t.hotkey}] ${t.label}`;
|
|
435
|
+
return currentTab === t.key ? theme.fg("accent", theme.bold(label)) : theme.fg("muted", label);
|
|
436
|
+
}).join(" ");
|
|
437
|
+
lines.push(` ${tabLine}`);
|
|
347
438
|
lines.push("");
|
|
348
439
|
|
|
349
|
-
// ── Tasks ──
|
|
350
440
|
const tasks = c.state?.tasks || [];
|
|
351
|
-
|
|
441
|
+
const streams = Array.from(c.antStreams.values());
|
|
442
|
+
|
|
443
|
+
// ── Tab: Tasks ──
|
|
444
|
+
if (currentTab === "tasks") {
|
|
445
|
+
const counts = {
|
|
446
|
+
done: tasks.filter(t => t.status === "done").length,
|
|
447
|
+
active: tasks.filter(t => t.status === "active").length,
|
|
448
|
+
failed: tasks.filter(t => t.status === "failed").length,
|
|
449
|
+
pending: tasks.filter(t => t.status === "pending" || t.status === "claimed" || t.status === "blocked").length,
|
|
450
|
+
};
|
|
352
451
|
lines.push(theme.fg("accent", " Tasks"));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
452
|
+
lines.push(theme.fg("muted", ` done:${counts.done} │ active:${counts.active} │ pending:${counts.pending} │ failed:${counts.failed}`));
|
|
453
|
+
lines.push(theme.fg("muted", " Filter: [0] all [a] active [d] done [f] failed"));
|
|
454
|
+
lines.push(theme.fg("muted", ` Current filter: ${taskFilter.toUpperCase()}`));
|
|
455
|
+
lines.push("");
|
|
456
|
+
|
|
457
|
+
const filtered = tasks.filter(t =>
|
|
458
|
+
taskFilter === "all" ? true :
|
|
459
|
+
taskFilter === "active" ? t.status === "active" :
|
|
460
|
+
taskFilter === "done" ? t.status === "done" :
|
|
461
|
+
t.status === "failed"
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
if (filtered.length === 0) {
|
|
465
|
+
lines.push(theme.fg("muted", " (no tasks match current filter)"));
|
|
466
|
+
} else {
|
|
467
|
+
for (const t of filtered.slice(0, 16)) {
|
|
468
|
+
const icon = t.status === "done" ? theme.fg("success", "✓")
|
|
469
|
+
: t.status === "failed" ? theme.fg("error", "✗")
|
|
470
|
+
: t.status === "active" ? theme.fg("warning", "●")
|
|
471
|
+
: theme.fg("dim", "○");
|
|
472
|
+
const dur = t.finishedAt && t.startedAt ? theme.fg("dim", ` ${formatDuration(t.finishedAt - t.startedAt)}`) : "";
|
|
473
|
+
lines.push(` ${icon} ${casteIcon(t.caste)} ${theme.fg("text", trim(t.title, w - 12))}${dur}`);
|
|
474
|
+
}
|
|
475
|
+
if (filtered.length > 16) lines.push(theme.fg("muted", ` ⋯ +${filtered.length - 16} more`));
|
|
360
476
|
}
|
|
361
|
-
if (tasks.length > 15) lines.push(theme.fg("muted", ` ⋯ +${tasks.length - 15} more`));
|
|
362
477
|
lines.push("");
|
|
363
478
|
}
|
|
364
479
|
|
|
365
|
-
// ──
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
lines.push(theme.fg("
|
|
480
|
+
// ── Tab: Streams ──
|
|
481
|
+
if (currentTab === "streams") {
|
|
482
|
+
lines.push(theme.fg("accent", ` Active Ant Streams (${streams.length})`));
|
|
483
|
+
lines.push(theme.fg("muted", " Shows latest line + token count for active ants"));
|
|
484
|
+
lines.push("");
|
|
485
|
+
if (streams.length === 0) {
|
|
486
|
+
lines.push(theme.fg("muted", " (no active streams right now)"));
|
|
487
|
+
} else {
|
|
488
|
+
for (const s of streams.slice(0, 10)) {
|
|
489
|
+
const excerpt = trim((s.lastLine || "...").replace(/\s+/g, " "), Math.max(20, w - 24));
|
|
490
|
+
lines.push(` ${casteIcon(s.caste)} ${theme.fg("muted", s.antId.slice(0, 12))} ${theme.fg("muted", `${formatTokens(s.tokens)}t`)} ${theme.fg("text", excerpt)}`);
|
|
491
|
+
}
|
|
492
|
+
if (streams.length > 10) lines.push(theme.fg("muted", ` ⋯ +${streams.length - 10} more streams`));
|
|
493
|
+
}
|
|
369
494
|
lines.push("");
|
|
370
495
|
}
|
|
371
496
|
|
|
372
|
-
|
|
373
|
-
|
|
497
|
+
// ── Tab: Log ──
|
|
498
|
+
if (currentTab === "log") {
|
|
499
|
+
const failedTasks = tasks.filter(t => t.status === "failed");
|
|
500
|
+
if (failedTasks.length > 0) {
|
|
501
|
+
lines.push(theme.fg("warning", ` Warnings (${failedTasks.length})`));
|
|
502
|
+
for (const t of failedTasks.slice(0, 4)) {
|
|
503
|
+
lines.push(` ${theme.fg("error", "✗")} ${theme.fg("text", trim(t.title, w - 8))}`);
|
|
504
|
+
}
|
|
505
|
+
if (failedTasks.length > 4) lines.push(theme.fg("muted", ` ⋯ +${failedTasks.length - 4} more failed tasks`));
|
|
506
|
+
lines.push("");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const recentLogs = c.logs.slice(-12);
|
|
510
|
+
lines.push(theme.fg("accent", " Recent Signals"));
|
|
511
|
+
if (recentLogs.length === 0) {
|
|
512
|
+
lines.push(theme.fg("muted", " (no signal logs yet)"));
|
|
513
|
+
} else {
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
for (const log of recentLogs) {
|
|
516
|
+
const age = formatDuration(Math.max(0, now - log.timestamp));
|
|
517
|
+
const levelIcon = log.level === "error" ? theme.fg("error", "✗") : log.level === "warning" ? theme.fg("warning", "!") : theme.fg("muted", "•");
|
|
518
|
+
lines.push(` ${levelIcon} ${theme.fg("muted", age)} ${theme.fg("text", trim(log.text, w - 12))}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
lines.push("");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
lines.push(theme.fg("muted", " [1/2/3] switch tabs │ [0/a/d/f] task filter │ esc close"));
|
|
374
525
|
return lines;
|
|
375
526
|
};
|
|
376
527
|
|
|
@@ -395,7 +546,21 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
395
546
|
if (matchesKey(data, "escape")) {
|
|
396
547
|
cleanup();
|
|
397
548
|
done(undefined);
|
|
549
|
+
return;
|
|
398
550
|
}
|
|
551
|
+
|
|
552
|
+
if (data === "1") currentTab = "tasks";
|
|
553
|
+
else if (data === "2") currentTab = "streams";
|
|
554
|
+
else if (data === "3") currentTab = "log";
|
|
555
|
+
else if (data === "0") taskFilter = "all";
|
|
556
|
+
else if (data.toLowerCase() === "a") taskFilter = "active";
|
|
557
|
+
else if (data.toLowerCase() === "d") taskFilter = "done";
|
|
558
|
+
else if (data.toLowerCase() === "f") taskFilter = "failed";
|
|
559
|
+
else return;
|
|
560
|
+
|
|
561
|
+
cachedWidth = undefined;
|
|
562
|
+
cachedLines = undefined;
|
|
563
|
+
tui.requestRender();
|
|
399
564
|
},
|
|
400
565
|
};
|
|
401
566
|
}, { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "80%" } });
|
|
@@ -471,7 +636,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
471
636
|
let text = theme.fg("toolTitle", theme.bold("🐜 ant_colony"));
|
|
472
637
|
if (args.maxAnts) text += theme.fg("muted", ` ×${args.maxAnts}`);
|
|
473
638
|
if (args.maxCost) text += theme.fg("warning", ` $${args.maxCost}`);
|
|
474
|
-
text += "\n" + theme.fg("
|
|
639
|
+
text += "\n" + theme.fg("muted", ` ${goal || "..."}`);
|
|
475
640
|
return new Text(text, 0, 0);
|
|
476
641
|
},
|
|
477
642
|
|
|
@@ -486,7 +651,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
486
651
|
0, 0,
|
|
487
652
|
));
|
|
488
653
|
if (activeColony) {
|
|
489
|
-
container.addChild(new Text(theme.fg("
|
|
654
|
+
container.addChild(new Text(theme.fg("muted", ` Goal: ${activeColony.goal.slice(0, 70)}`), 0, 0));
|
|
490
655
|
container.addChild(new Text(theme.fg("muted", ` Ctrl+Shift+A for details │ /colony-stop to cancel`), 0, 0));
|
|
491
656
|
}
|
|
492
657
|
return container;
|
|
@@ -502,12 +667,19 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
502
667
|
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
503
668
|
const m = state?.metrics;
|
|
504
669
|
const phase = state?.status || "scouting";
|
|
670
|
+
const progress = calcProgress(m);
|
|
671
|
+
const pct = Math.round(progress * 100);
|
|
672
|
+
const activeAnts = c.antStreams.size;
|
|
505
673
|
|
|
506
674
|
const lines: string[] = [
|
|
507
|
-
`🐜 ${statusIcon(phase)} ${c.goal
|
|
508
|
-
`${phase} │ ${m ? `${m.tasksDone}/${m.tasksTotal} tasks` : "starting"} │ ${m ? formatCost(m.totalCost) : "$0"} │ ${elapsed}`,
|
|
675
|
+
`🐜 ${statusIcon(phase)} ${trim(c.goal, 80)}`,
|
|
676
|
+
`${statusLabel(phase)} │ ${m ? `${m.tasksDone}/${m.tasksTotal} tasks` : "starting"} │ ${pct}% │ ⚡${activeAnts} │ ${m ? formatCost(m.totalCost) : "$0"} │ ${elapsed}`,
|
|
677
|
+
`${progressBar(progress, 18)} ${pct}%`,
|
|
509
678
|
];
|
|
510
679
|
|
|
680
|
+
if (c.phase && c.phase !== "initializing") lines.push(`Phase: ${trim(c.phase, 100)}`);
|
|
681
|
+
const lastLog = c.logs[c.logs.length - 1];
|
|
682
|
+
if (lastLog) lines.push(`Last: ${trim(lastLog.text, 100)}`);
|
|
511
683
|
if (m && m.tasksFailed > 0) lines.push(`⚠ ${m.tasksFailed} failed`);
|
|
512
684
|
|
|
513
685
|
return lines.join("\n");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatDuration, formatCost, formatTokens, statusIcon, casteIcon, buildReport } from "./ui.js";
|
|
2
|
+
import { formatDuration, formatCost, formatTokens, statusIcon, statusLabel, progressBar, casteIcon, buildReport } from "./ui.js";
|
|
3
3
|
import type { ColonyState } from "./types.js";
|
|
4
4
|
|
|
5
5
|
describe("formatDuration", () => {
|
|
@@ -25,16 +25,33 @@ describe("formatTokens", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
describe("statusIcon", () => {
|
|
28
|
+
it("launched", () => expect(statusIcon("launched")).toBe("🚀"));
|
|
28
29
|
it("scouting", () => expect(statusIcon("scouting")).toBe("🔍"));
|
|
29
30
|
it("working", () => expect(statusIcon("working")).toBe("⚒️"));
|
|
30
31
|
it("planning_recovery", () => expect(statusIcon("planning_recovery")).toBe("♻️"));
|
|
31
32
|
it("reviewing", () => expect(statusIcon("reviewing")).toBe("🛡️"));
|
|
33
|
+
it("task_done", () => expect(statusIcon("task_done")).toBe("✅"));
|
|
32
34
|
it("done", () => expect(statusIcon("done")).toBe("✅"));
|
|
33
35
|
it("failed", () => expect(statusIcon("failed")).toBe("❌"));
|
|
34
36
|
it("budget_exceeded", () => expect(statusIcon("budget_exceeded")).toBe("💰"));
|
|
35
37
|
it("unknown", () => expect(statusIcon("xyz")).toBe("🐜"));
|
|
36
38
|
});
|
|
37
39
|
|
|
40
|
+
describe("statusLabel", () => {
|
|
41
|
+
it("launched", () => expect(statusLabel("launched")).toBe("LAUNCHED"));
|
|
42
|
+
it("scouting", () => expect(statusLabel("scouting")).toBe("SCOUTING"));
|
|
43
|
+
it("planning_recovery", () => expect(statusLabel("planning_recovery")).toBe("PLANNING_RECOVERY"));
|
|
44
|
+
it("task_done", () => expect(statusLabel("task_done")).toBe("TASK_DONE"));
|
|
45
|
+
it("budget_exceeded", () => expect(statusLabel("budget_exceeded")).toBe("BUDGET_EXCEEDED"));
|
|
46
|
+
it("unknown", () => expect(statusLabel("custom")).toBe("CUSTOM"));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("progressBar", () => {
|
|
50
|
+
it("0%", () => expect(progressBar(0, 10)).toBe("[----------]"));
|
|
51
|
+
it("50%", () => expect(progressBar(0.5, 10)).toBe("[#####-----]"));
|
|
52
|
+
it("100%", () => expect(progressBar(1, 10)).toBe("[##########]"));
|
|
53
|
+
});
|
|
54
|
+
|
|
38
55
|
describe("casteIcon", () => {
|
|
39
56
|
it("scout", () => expect(casteIcon("scout")).toBe("🔍"));
|
|
40
57
|
it("soldier", () => expect(casteIcon("soldier")).toBe("🛡️"));
|
|
@@ -17,12 +17,42 @@ export function formatTokens(n: number): string {
|
|
|
17
17
|
|
|
18
18
|
export function statusIcon(status: string): string {
|
|
19
19
|
const icons: Record<string, string> = {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
launched: "🚀",
|
|
21
|
+
scouting: "🔍",
|
|
22
|
+
planning_recovery: "♻️",
|
|
23
|
+
working: "⚒️",
|
|
24
|
+
reviewing: "🛡️",
|
|
25
|
+
task_done: "✅",
|
|
26
|
+
done: "✅",
|
|
27
|
+
complete: "✅",
|
|
28
|
+
failed: "❌",
|
|
29
|
+
budget_exceeded: "💰",
|
|
22
30
|
};
|
|
23
31
|
return icons[status] || "🐜";
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
export function statusLabel(status: string): string {
|
|
35
|
+
const labels: Record<string, string> = {
|
|
36
|
+
launched: "LAUNCHED",
|
|
37
|
+
scouting: "SCOUTING",
|
|
38
|
+
planning_recovery: "PLANNING_RECOVERY",
|
|
39
|
+
working: "WORKING",
|
|
40
|
+
reviewing: "REVIEWING",
|
|
41
|
+
task_done: "TASK_DONE",
|
|
42
|
+
done: "DONE",
|
|
43
|
+
complete: "COMPLETE",
|
|
44
|
+
failed: "FAILED",
|
|
45
|
+
budget_exceeded: "BUDGET_EXCEEDED",
|
|
46
|
+
};
|
|
47
|
+
return labels[status] || status.toUpperCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function progressBar(progress: number, width = 14): string {
|
|
51
|
+
const p = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));
|
|
52
|
+
const filled = Math.round(width * p);
|
|
53
|
+
return `[${"#".repeat(filled)}${"-".repeat(Math.max(0, width - filled))}]`;
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
export function casteIcon(caste: string): string {
|
|
27
57
|
return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : caste === "drone" ? "⚙️" : "⚒️";
|
|
28
58
|
}
|