pi-thread-engine 0.1.0
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/PLAN.md +43 -0
- package/README.md +108 -0
- package/extensions/index.ts +641 -0
- package/package.json +36 -0
- package/src/core/executor.ts +200 -0
- package/src/core/registry.ts +281 -0
- package/src/core/types.ts +104 -0
- package/src/dashboard.ts +223 -0
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive TUI dashboard for pi-threads.
|
|
3
|
+
* Shows threads + stories in a navigable overlay.
|
|
4
|
+
*
|
|
5
|
+
* Keys: ↑↓ navigate, Enter expand, k kill, r review, p prune, q/Esc close
|
|
6
|
+
*/
|
|
7
|
+
import { Container, Text, Spacer, matchesKey, Key, truncateToWidth } from "@mariozechner/pi-tui";
|
|
8
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { ThreadRegistry } from "./core/registry.js";
|
|
10
|
+
import type { Thread, Story, ThreadSummary } from "./core/types.js";
|
|
11
|
+
|
|
12
|
+
export interface DashboardTheme {
|
|
13
|
+
fg: (color: string, text: string) => string;
|
|
14
|
+
bold: (text: string) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DashboardRow {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: "thread" | "story";
|
|
20
|
+
label: string;
|
|
21
|
+
display: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createDashboard(
|
|
25
|
+
registry: ThreadRegistry,
|
|
26
|
+
theme: DashboardTheme,
|
|
27
|
+
onClose: () => void,
|
|
28
|
+
onKill?: (id: string) => void,
|
|
29
|
+
onReview?: (id: string) => void
|
|
30
|
+
) {
|
|
31
|
+
let selected = 0;
|
|
32
|
+
let expanded: string | null = null;
|
|
33
|
+
let rows: DashboardRow[] = [];
|
|
34
|
+
let cachedWidth: number | undefined;
|
|
35
|
+
let cachedLines: string[] | undefined;
|
|
36
|
+
|
|
37
|
+
function stateIcon(state: string): string {
|
|
38
|
+
switch (state) {
|
|
39
|
+
case "running": return "⟳";
|
|
40
|
+
case "completed": return "✓";
|
|
41
|
+
case "failed": case "killed": return "✗";
|
|
42
|
+
case "pending": return "·";
|
|
43
|
+
case "planning": return "📋";
|
|
44
|
+
case "executing": return "⚡";
|
|
45
|
+
case "verifying": return "🔍";
|
|
46
|
+
case "done": return "✅";
|
|
47
|
+
default: return "?";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stateColor(state: string): string {
|
|
52
|
+
switch (state) {
|
|
53
|
+
case "running": case "executing": return "warning";
|
|
54
|
+
case "completed": case "done": return "success";
|
|
55
|
+
case "failed": case "killed": return "error";
|
|
56
|
+
default: return "muted";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function typeIcon(type: string): string {
|
|
61
|
+
switch (type) {
|
|
62
|
+
case "parallel": return "⫘";
|
|
63
|
+
case "chained": return "⟶";
|
|
64
|
+
case "fusion": return "⊕";
|
|
65
|
+
case "meta": return "◎";
|
|
66
|
+
case "long": return "∞";
|
|
67
|
+
case "zero": return "⊘";
|
|
68
|
+
default: return "·";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildRows(): DashboardRow[] {
|
|
73
|
+
const result: DashboardRow[] = [];
|
|
74
|
+
|
|
75
|
+
// Stories first
|
|
76
|
+
for (const s of registry.allStories()) {
|
|
77
|
+
const phases = s.phases.map((p) => `${stateIcon(p.state)}${p.name}`).join("→");
|
|
78
|
+
const display = `📖 ${theme.fg("accent", s.id)} [${theme.fg(stateColor(s.state), s.state)}] ${s.goal.slice(0, 45)} ${theme.fg("dim", phases)}`;
|
|
79
|
+
result.push({ id: s.id, kind: "story", label: s.goal, display });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Then threads
|
|
83
|
+
for (const t of registry.all()) {
|
|
84
|
+
const sum = registry.summarize(t);
|
|
85
|
+
const be = sum.backend === "subagent" ? theme.fg("dim", " [sub]") : "";
|
|
86
|
+
const display = `${typeIcon(sum.type)} ${theme.fg("accent", sum.id)} ${sum.type} [${theme.fg(stateColor(sum.state), sum.state)}] ${sum.progress} (${sum.elapsed})${be} ${sum.label.slice(0, 40)}`;
|
|
87
|
+
result.push({ id: sum.id, kind: "thread", label: sum.label, display });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderExpanded(id: string, width: number): string[] {
|
|
94
|
+
const lines: string[] = [];
|
|
95
|
+
const indent = " ";
|
|
96
|
+
const maxW = width - 6;
|
|
97
|
+
|
|
98
|
+
// Thread detail
|
|
99
|
+
const t = registry.get(id);
|
|
100
|
+
if (t) {
|
|
101
|
+
lines.push(theme.fg("accent", theme.bold(` Thread ${t.id} (${t.type}) — ${t.state}`)));
|
|
102
|
+
lines.push("");
|
|
103
|
+
for (const task of t.tasks) {
|
|
104
|
+
const icon = stateIcon(task.state);
|
|
105
|
+
const color = stateColor(task.state);
|
|
106
|
+
lines.push(theme.fg(color, `${indent}${icon} ${task.id}: ${truncateToWidth(task.label, maxW)}`));
|
|
107
|
+
if (task.model) lines.push(theme.fg("dim", `${indent} model: ${task.model}`));
|
|
108
|
+
if (task.result) {
|
|
109
|
+
const preview = task.result.replace(/\n/g, " ").slice(0, 200);
|
|
110
|
+
lines.push(theme.fg("muted", `${indent} → ${truncateToWidth(preview, maxW)}`));
|
|
111
|
+
}
|
|
112
|
+
if (task.error) {
|
|
113
|
+
lines.push(theme.fg("error", `${indent} ✗ ${truncateToWidth(task.error, maxW)}`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Story detail
|
|
120
|
+
const s = registry.getStory(id);
|
|
121
|
+
if (s) {
|
|
122
|
+
lines.push(theme.fg("accent", theme.bold(` Story ${s.id} — ${s.state}`)));
|
|
123
|
+
lines.push(theme.fg("muted", ` ${s.goal}`));
|
|
124
|
+
lines.push("");
|
|
125
|
+
for (const phase of s.phases) {
|
|
126
|
+
const icon = stateIcon(phase.state);
|
|
127
|
+
const color = stateColor(phase.state);
|
|
128
|
+
const tid = phase.threadId ? theme.fg("dim", ` [${phase.threadId}]`) : "";
|
|
129
|
+
lines.push(theme.fg(color, `${indent}${icon} ${phase.name} (${phase.threadType})${tid}`));
|
|
130
|
+
lines.push(theme.fg("dim", `${indent} ${truncateToWidth(phase.description, maxW)}`));
|
|
131
|
+
}
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return [theme.fg("error", ` ${id} not found`)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const component = {
|
|
139
|
+
handleInput(data: string) {
|
|
140
|
+
if (matchesKey(data, Key.escape) || data === "q") {
|
|
141
|
+
onClose();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (matchesKey(data, Key.up) && selected > 0) {
|
|
145
|
+
selected--;
|
|
146
|
+
expanded = null;
|
|
147
|
+
cachedWidth = undefined;
|
|
148
|
+
}
|
|
149
|
+
if (matchesKey(data, Key.down) && selected < rows.length - 1) {
|
|
150
|
+
selected++;
|
|
151
|
+
expanded = null;
|
|
152
|
+
cachedWidth = undefined;
|
|
153
|
+
}
|
|
154
|
+
if (matchesKey(data, Key.enter) && rows[selected]) {
|
|
155
|
+
expanded = expanded === rows[selected].id ? null : rows[selected].id;
|
|
156
|
+
cachedWidth = undefined;
|
|
157
|
+
}
|
|
158
|
+
if (data === "k" && rows[selected]) {
|
|
159
|
+
onKill?.(rows[selected].id);
|
|
160
|
+
cachedWidth = undefined;
|
|
161
|
+
}
|
|
162
|
+
if (data === "r" && rows[selected]) {
|
|
163
|
+
onReview?.(rows[selected].id);
|
|
164
|
+
cachedWidth = undefined;
|
|
165
|
+
}
|
|
166
|
+
if (data === "p") {
|
|
167
|
+
registry.prune();
|
|
168
|
+
cachedWidth = undefined;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
render(width: number): string[] {
|
|
173
|
+
// Rebuild rows every render (state changes)
|
|
174
|
+
rows = buildRows();
|
|
175
|
+
|
|
176
|
+
if (selected >= rows.length) selected = Math.max(0, rows.length - 1);
|
|
177
|
+
|
|
178
|
+
const lines: string[] = [];
|
|
179
|
+
const border = "─".repeat(Math.min(width - 4, 80));
|
|
180
|
+
|
|
181
|
+
// Header
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push(theme.fg("accent", theme.bold(" 🧵 Thread Dashboard")));
|
|
184
|
+
lines.push(theme.fg("dim", ` ${border}`));
|
|
185
|
+
|
|
186
|
+
if (rows.length === 0) {
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push(theme.fg("muted", " No threads or stories."));
|
|
189
|
+
lines.push(theme.fg("dim", " Use /pthread /fthread /zthread /story to start."));
|
|
190
|
+
} else {
|
|
191
|
+
lines.push("");
|
|
192
|
+
for (let i = 0; i < rows.length; i++) {
|
|
193
|
+
const prefix = i === selected ? theme.fg("accent", " ▸ ") : " ";
|
|
194
|
+
const row = rows[i];
|
|
195
|
+
const line = prefix + truncateToWidth(row.display, width - 4);
|
|
196
|
+
lines.push(line);
|
|
197
|
+
|
|
198
|
+
// Show expanded detail
|
|
199
|
+
if (expanded === row.id) {
|
|
200
|
+
lines.push(...renderExpanded(row.id, width));
|
|
201
|
+
lines.push("");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Footer
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push(theme.fg("dim", ` ${border}`));
|
|
209
|
+
const help = "↑↓ navigate Enter expand k kill r review p prune q close";
|
|
210
|
+
lines.push(theme.fg("dim", ` ${help}`));
|
|
211
|
+
lines.push("");
|
|
212
|
+
|
|
213
|
+
return lines;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
invalidate() {
|
|
217
|
+
cachedWidth = undefined;
|
|
218
|
+
cachedLines = undefined;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return component;
|
|
223
|
+
}
|