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 CHANGED
@@ -22,20 +22,28 @@ npx oh-pi
22
22
 
23
23
  ---
24
24
 
25
- ## Pourquoi
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
- C'est tout. oh-pi détecte votre environnement, vous guide dans la configuration et écrit `~/.pi/agent/` pour vous.
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
- Vous avez déjà une config ? oh-pi la détecte et propose une **sauvegarde avant écrasement**.
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 avec la conversation principale via des signaux structurés, empêchant le LLM de vérifier ou deviner l'état :
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 ne pas vérifier |
147
- | `COLONY_SIGNAL:RUNNING` | Colonie active injecté à chaque tour |
148
- | `COLONY_SIGNAL:COMPLETE` | Colonie terminée consulter le rapport |
149
- | `COLONY_SIGNAL:FAILED` | Colonie crashée signaler l'erreur |
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
- ## Why
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
- That's it. oh-pi detects your environment, walks you through setup, and writes `~/.pi/agent/` for you.
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
- Already have a config? oh-pi detects it and offers **backup before overwriting**.
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, preventing the LLM from polling or guessing colony status:
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 don't poll |
147
- | `COLONY_SIGNAL:RUNNING` | Colony active injected each turn |
148
- | `COLONY_SIGNAL:COMPLETE` | Colony finished review report |
149
- | `COLONY_SIGNAL:FAILED` | Colony crashed report error |
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 检测你的环境,引导你完成设置,自动写入 `~/.pi/agent/`。
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
- 已有配置?oh-pi 会检测到并提供**覆盖前备份**。
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
- 蚁群通过结构化信号与主对话通信,防止 LLM 轮询或猜测蚁群状态:
166
+ 蚁群通过结构化信号与主对话通信,让模型无需猜测后台状态:
143
167
 
144
168
  | 信号 | 含义 |
145
169
  |------|------|
146
- | `COLONY_SIGNAL:LAUNCHED` | 蚁群已启动 — 不要轮询 |
147
- | `COLONY_SIGNAL:RUNNING` | 蚁群运行中 — 每轮注入 |
148
- | `COLONY_SIGNAL:COMPLETE` | 蚁群完成 — 查看报告 |
149
- | `COLONY_SIGNAL:FAILED` | 蚁群崩溃 — 报告错误 |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.75",
3
+ "version": "0.1.76",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 colonyStatus = state?.status || "scouting";
82
-
83
- const parts = [`🐜 ${statusIcon(colonyStatus)}`];
84
- if (m) parts.push(`${m.tasksDone}/${m.tasksTotal}`);
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("dim", l.slice(4).trim().slice(0, 70))}`, 0, 0));
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 cost = c.state ? formatCost(c.state.metrics.totalCost) : "$0";
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("dim", ` Goal: ${c.goal.slice(0, w - 8)}`));
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
- if (tasks.length > 0) {
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
- for (const t of tasks.slice(0, 15)) {
354
- const icon = t.status === "done" ? theme.fg("success", "")
355
- : t.status === "failed" ? theme.fg("error", "✗")
356
- : t.status === "active" ? theme.fg("warning", "●")
357
- : theme.fg("dim", "○");
358
- const dur = t.finishedAt && t.startedAt ? theme.fg("dim", ` ${formatDuration(t.finishedAt - t.startedAt)}`) : "";
359
- lines.push(` ${icon} ${casteIcon(t.caste)} ${theme.fg("text", t.title.slice(0, w - 12))}${dur}`);
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
- // ── Active Ants ──
366
- const streams = Array.from(c.antStreams.values());
367
- if (streams.length > 0) {
368
- lines.push(theme.fg("accent", ` Active: ${streams.length} ants working`));
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
- lines.push("");
373
- lines.push(theme.fg("muted", " esc close"));
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("dim", ` ${goal || "..."}`);
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("dim", ` Goal: ${activeColony.goal.slice(0, 70)}`), 0, 0));
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.slice(0, 80)}`,
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
- scouting: "🔍", planning_recovery: "♻️", working: "⚒️", reviewing: "🛡️",
21
- done: "", failed: "❌", budget_exceeded: "💰",
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
  }