shariq-pi-extensions 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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type GoalStatus = "active" | "paused" | "blocked" | "usage_limited" | "budget_limited" | "complete";
|
|
2
|
+
export type GoalProgressStatus = "pending" | "in_progress" | "complete" | "blocked";
|
|
3
|
+
export type GoalSuppressionReason = "no_tool_progress" | "interrupted" | "error";
|
|
4
|
+
|
|
5
|
+
export type GoalProgressItem = {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
status: GoalProgressStatus;
|
|
9
|
+
evidence?: string;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type GoalContinuationSuppression = {
|
|
14
|
+
reason: GoalSuppressionReason;
|
|
15
|
+
message: string;
|
|
16
|
+
at: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type GoalState = {
|
|
20
|
+
id: string;
|
|
21
|
+
objective: string;
|
|
22
|
+
status: GoalStatus;
|
|
23
|
+
tokenBudget: number | null;
|
|
24
|
+
tokensUsed: number;
|
|
25
|
+
timeUsedSeconds: number;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
activeStartedAt: number | null;
|
|
29
|
+
progress: GoalProgressItem[];
|
|
30
|
+
blockedTurnStreak: number;
|
|
31
|
+
blockedSignature: string | null;
|
|
32
|
+
continuationSuppressed: GoalContinuationSuppression | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type GoalSnapshot = {
|
|
36
|
+
goal: GoalState | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type GoalCreateParams = {
|
|
40
|
+
objective: string;
|
|
41
|
+
token_budget?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type GoalUpdateParams = {
|
|
45
|
+
status: "complete" | "blocked";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type GoalProgressParams = {
|
|
49
|
+
items: Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
status: GoalProgressStatus;
|
|
53
|
+
evidence?: string;
|
|
54
|
+
}>;
|
|
55
|
+
remove_ids?: string[];
|
|
56
|
+
};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionCommandContext,
|
|
3
|
+
KeybindingsManager,
|
|
4
|
+
Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
7
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
8
|
+
import {
|
|
9
|
+
frameBottom,
|
|
10
|
+
framedRow,
|
|
11
|
+
frameTop,
|
|
12
|
+
joinSides,
|
|
13
|
+
meter,
|
|
14
|
+
oneLine,
|
|
15
|
+
padLine,
|
|
16
|
+
stateLabel,
|
|
17
|
+
type SemanticState,
|
|
18
|
+
viewportSlice,
|
|
19
|
+
} from "../shared/tui-dashboard.ts";
|
|
20
|
+
import type { GoalProgressItem, GoalState, GoalStatus } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
export type GoalDashboardAction =
|
|
23
|
+
| "close"
|
|
24
|
+
| "edit"
|
|
25
|
+
| "pause"
|
|
26
|
+
| "resume"
|
|
27
|
+
| "clear";
|
|
28
|
+
|
|
29
|
+
interface GoalDashboardOptions {
|
|
30
|
+
getGoal(): GoalState | null;
|
|
31
|
+
elapsedSeconds(goal: GoalState): number;
|
|
32
|
+
formatTokens(tokens: number): string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function configuredKeys(
|
|
36
|
+
keybindings: KeybindingsManager,
|
|
37
|
+
binding: Parameters<KeybindingsManager["getKeys"]>[0],
|
|
38
|
+
): string {
|
|
39
|
+
return keybindings.getKeys(binding).join("/") || "unbound";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function statusState(status: GoalStatus): SemanticState {
|
|
43
|
+
switch (status) {
|
|
44
|
+
case "active":
|
|
45
|
+
return "active";
|
|
46
|
+
case "complete":
|
|
47
|
+
return "success";
|
|
48
|
+
case "blocked":
|
|
49
|
+
case "usage_limited":
|
|
50
|
+
case "budget_limited":
|
|
51
|
+
return "warning";
|
|
52
|
+
case "paused":
|
|
53
|
+
return "muted";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function progressState(item: GoalProgressItem): SemanticState {
|
|
58
|
+
switch (item.status) {
|
|
59
|
+
case "complete":
|
|
60
|
+
return "success";
|
|
61
|
+
case "blocked":
|
|
62
|
+
return "error";
|
|
63
|
+
case "in_progress":
|
|
64
|
+
return "active";
|
|
65
|
+
case "pending":
|
|
66
|
+
return "muted";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function progressColor(item: GoalProgressItem): "accent" | "success" | "warning" | "error" | "muted" {
|
|
71
|
+
switch (progressState(item)) {
|
|
72
|
+
case "active":
|
|
73
|
+
return "accent";
|
|
74
|
+
case "success":
|
|
75
|
+
return "success";
|
|
76
|
+
case "error":
|
|
77
|
+
return "error";
|
|
78
|
+
case "warning":
|
|
79
|
+
return "warning";
|
|
80
|
+
case "muted":
|
|
81
|
+
return "muted";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function progressGlyph(item: GoalProgressItem): string {
|
|
86
|
+
switch (item.status) {
|
|
87
|
+
case "complete":
|
|
88
|
+
return "✓";
|
|
89
|
+
case "blocked":
|
|
90
|
+
return "!";
|
|
91
|
+
case "in_progress":
|
|
92
|
+
return "◆";
|
|
93
|
+
case "pending":
|
|
94
|
+
return "○";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatElapsed(seconds: number): string {
|
|
99
|
+
const hours = Math.floor(seconds / 3600);
|
|
100
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
101
|
+
const remainder = seconds % 60;
|
|
102
|
+
if (hours > 0) return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
|
|
103
|
+
if (minutes > 0) return `${minutes}m ${remainder.toString().padStart(2, "0")}s`;
|
|
104
|
+
return `${remainder}s`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class GoalDashboard implements Component {
|
|
108
|
+
private selected = 0;
|
|
109
|
+
private closed = false;
|
|
110
|
+
private readonly ticker: ReturnType<typeof setInterval>;
|
|
111
|
+
private readonly tui: TUI;
|
|
112
|
+
private readonly theme: Theme;
|
|
113
|
+
private readonly keybindings: KeybindingsManager;
|
|
114
|
+
private readonly options: GoalDashboardOptions;
|
|
115
|
+
private readonly done: (action: GoalDashboardAction) => void;
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
tui: TUI,
|
|
119
|
+
theme: Theme,
|
|
120
|
+
keybindings: KeybindingsManager,
|
|
121
|
+
options: GoalDashboardOptions,
|
|
122
|
+
done: (action: GoalDashboardAction) => void,
|
|
123
|
+
) {
|
|
124
|
+
this.tui = tui;
|
|
125
|
+
this.theme = theme;
|
|
126
|
+
this.keybindings = keybindings;
|
|
127
|
+
this.options = options;
|
|
128
|
+
this.done = done;
|
|
129
|
+
this.ticker = setInterval(() => this.tui.requestRender(), 1000);
|
|
130
|
+
this.ticker.unref?.();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private close(action: GoalDashboardAction): void {
|
|
134
|
+
if (this.closed) return;
|
|
135
|
+
this.closed = true;
|
|
136
|
+
clearInterval(this.ticker);
|
|
137
|
+
this.done(action);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
dispose(): void {
|
|
141
|
+
if (!this.closed) clearInterval(this.ticker);
|
|
142
|
+
this.closed = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
handleInput(data: string): void {
|
|
146
|
+
const goal = this.options.getGoal();
|
|
147
|
+
const progress = goal?.progress ?? [];
|
|
148
|
+
this.selected = Math.min(this.selected, Math.max(0, progress.length - 1));
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
this.keybindings.matches(data, "tui.select.cancel") ||
|
|
152
|
+
data === "q"
|
|
153
|
+
) {
|
|
154
|
+
this.close("close");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (this.keybindings.matches(data, "tui.select.up") || data === "k") {
|
|
158
|
+
if (progress.length > 0) {
|
|
159
|
+
this.selected = (this.selected - 1 + progress.length) % progress.length;
|
|
160
|
+
this.tui.requestRender();
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (this.keybindings.matches(data, "tui.select.down") || data === "j") {
|
|
165
|
+
if (progress.length > 0) {
|
|
166
|
+
this.selected = (this.selected + 1) % progress.length;
|
|
167
|
+
this.tui.requestRender();
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (data === "e") {
|
|
172
|
+
this.close("edit");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (data === "x") {
|
|
176
|
+
this.close("clear");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (data === "p" || data === " ") {
|
|
180
|
+
if (goal?.status === "active") this.close("pause");
|
|
181
|
+
else if (goal && goal.status !== "complete" && goal.status !== "budget_limited") {
|
|
182
|
+
this.close("resume");
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (data === "r" && goal && goal.status !== "active" && goal.status !== "complete") {
|
|
187
|
+
this.close("resume");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
render(width: number): string[] {
|
|
192
|
+
const theme = this.theme;
|
|
193
|
+
const goal = this.options.getGoal();
|
|
194
|
+
if (!goal) {
|
|
195
|
+
return [
|
|
196
|
+
frameTop(theme, width, "Goal"),
|
|
197
|
+
framedRow(theme, theme.fg("muted", "No goal is currently set."), width),
|
|
198
|
+
frameBottom(theme, width),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rows = this.tui.terminal.rows || 30;
|
|
203
|
+
const completed = goal.progress.filter((item) => item.status === "complete").length;
|
|
204
|
+
const current = goal.progress.find((item) => item.status === "in_progress");
|
|
205
|
+
const status = stateLabel(theme, statusState(goal.status), goal.status.replaceAll("_", " "));
|
|
206
|
+
const elapsed = formatElapsed(this.options.elapsedSeconds(goal));
|
|
207
|
+
const titleLeft = theme.fg("accent", theme.bold("Goal control center"));
|
|
208
|
+
const titleRight = `${status}${theme.fg("dim", ` · ${elapsed}`)}`;
|
|
209
|
+
const lines: string[] = [joinSides(` ${titleLeft}`, `${titleRight} `, width)];
|
|
210
|
+
|
|
211
|
+
const progressText = goal.progress.length > 0
|
|
212
|
+
? `${completed}/${goal.progress.length} complete`
|
|
213
|
+
: "checklist not started";
|
|
214
|
+
const progressMeter = goal.progress.length > 0
|
|
215
|
+
? meter(theme, completed, goal.progress.length, Math.min(28, Math.max(12, width - 48)), completed === goal.progress.length ? "success" : "active")
|
|
216
|
+
: theme.fg("borderMuted", "────────");
|
|
217
|
+
lines.push(
|
|
218
|
+
joinSides(
|
|
219
|
+
` ${theme.fg("muted", "PROGRESS")} ${progressMeter} ${theme.fg("text", progressText)}`,
|
|
220
|
+
goal.tokenBudget == null
|
|
221
|
+
? `${theme.fg("muted", "TOKENS")} ${theme.fg("text", this.options.formatTokens(goal.tokensUsed))} `
|
|
222
|
+
: `${theme.fg("muted", "BUDGET")} ${theme.fg("text", `${this.options.formatTokens(goal.tokensUsed)} / ${this.options.formatTokens(goal.tokenBudget)}`)} `,
|
|
223
|
+
width,
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
if (goal.tokenBudget != null && width >= 72) {
|
|
227
|
+
lines.push(
|
|
228
|
+
` ${theme.fg("muted", "TOKEN BUDGET")} ${meter(theme, goal.tokensUsed, goal.tokenBudget, Math.max(12, width - 24), goal.tokensUsed >= goal.tokenBudget ? "warning" : "active")}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push(frameTop(theme, width, "objective"));
|
|
233
|
+
const objectiveWidth = Math.max(10, width - 4);
|
|
234
|
+
const objectiveLines = wrapTextWithAnsi(oneLine(goal.objective), objectiveWidth).slice(0, 3);
|
|
235
|
+
for (const line of objectiveLines) {
|
|
236
|
+
lines.push(framedRow(theme, ` ${theme.fg("text", line)}`, width));
|
|
237
|
+
}
|
|
238
|
+
if (objectiveLines.length === 0) lines.push(framedRow(theme, "", width));
|
|
239
|
+
lines.push(frameBottom(theme, width));
|
|
240
|
+
|
|
241
|
+
const chromeRows = lines.length + 5;
|
|
242
|
+
const detailRows = current || goal.continuationSuppressed || goal.blockedTurnStreak > 0 ? 2 : 0;
|
|
243
|
+
const listHeight = Math.max(3, rows - chromeRows - detailRows);
|
|
244
|
+
lines.push(frameTop(theme, width, `checklist · ${completed}/${goal.progress.length}`));
|
|
245
|
+
|
|
246
|
+
if (goal.progress.length === 0) {
|
|
247
|
+
lines.push(
|
|
248
|
+
framedRow(
|
|
249
|
+
theme,
|
|
250
|
+
` ${theme.fg("muted", "No checklist yet — the agent creates one for meaningful multi-step work.")}`,
|
|
251
|
+
width,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
for (let index = 1; index < listHeight; index++) lines.push(framedRow(theme, "", width));
|
|
255
|
+
} else {
|
|
256
|
+
this.selected = Math.min(this.selected, goal.progress.length - 1);
|
|
257
|
+
const viewport = viewportSlice(goal.progress, this.selected, listHeight);
|
|
258
|
+
for (let row = 0; row < listHeight; row++) {
|
|
259
|
+
const item = viewport.items[row];
|
|
260
|
+
if (!item) {
|
|
261
|
+
lines.push(framedRow(theme, "", width));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const index = viewport.start + row;
|
|
265
|
+
const selected = index === this.selected;
|
|
266
|
+
const marker = selected ? theme.fg("accent", "❯") : " ";
|
|
267
|
+
const glyph = theme.fg(progressColor(item), progressGlyph(item));
|
|
268
|
+
const id = theme.fg("dim", `[${oneLine(item.id)}]`);
|
|
269
|
+
const itemStatus = theme.fg(
|
|
270
|
+
progressColor(item),
|
|
271
|
+
item.status.replaceAll("_", " "),
|
|
272
|
+
);
|
|
273
|
+
const right = `${itemStatus} `;
|
|
274
|
+
const left = ` ${marker} ${glyph} ${id} ${selected ? theme.fg("accent", oneLine(item.title)) : theme.fg("text", oneLine(item.title))}`;
|
|
275
|
+
lines.push(framedRow(theme, joinSides(left, right, Math.max(0, width - 2)), width, selected));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
lines.push(frameBottom(theme, width));
|
|
279
|
+
|
|
280
|
+
const selectedItem = goal.progress[this.selected];
|
|
281
|
+
if (selectedItem?.evidence) {
|
|
282
|
+
lines.push(
|
|
283
|
+
truncateToWidth(
|
|
284
|
+
` ${theme.fg("success", "evidence")} ${theme.fg("muted", oneLine(selectedItem.evidence))}`,
|
|
285
|
+
width,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
} else if (goal.continuationSuppressed) {
|
|
289
|
+
lines.push(
|
|
290
|
+
truncateToWidth(
|
|
291
|
+
` ${theme.fg("warning", "waiting")} ${theme.fg("muted", oneLine(goal.continuationSuppressed.message))}`,
|
|
292
|
+
width,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
} else if (goal.blockedTurnStreak > 0) {
|
|
296
|
+
lines.push(
|
|
297
|
+
` ${theme.fg("warning", `blocked gate ${goal.blockedTurnStreak}/3 turns`)}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const toggle = goal.status === "active" ? "p pause" : goal.status === "complete" ? "" : "r resume";
|
|
302
|
+
const hints = [
|
|
303
|
+
`${configuredKeys(this.keybindings, "tui.select.up")}/${configuredKeys(this.keybindings, "tui.select.down")}/jk select`,
|
|
304
|
+
toggle,
|
|
305
|
+
"e edit",
|
|
306
|
+
"x clear",
|
|
307
|
+
`${configuredKeys(this.keybindings, "tui.select.cancel")} close`,
|
|
308
|
+
].filter(Boolean).join(" · ");
|
|
309
|
+
lines.push(truncateToWidth(theme.fg("dim", ` ${hints}`), width));
|
|
310
|
+
return lines.map((line) => padLine(line, width));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
invalidate(): void {}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function openGoalDashboard(
|
|
317
|
+
ctx: ExtensionCommandContext,
|
|
318
|
+
options: GoalDashboardOptions,
|
|
319
|
+
): Promise<GoalDashboardAction> {
|
|
320
|
+
if (ctx.mode !== "tui") return "close";
|
|
321
|
+
return ctx.ui.custom<GoalDashboardAction>(
|
|
322
|
+
(tui, theme, keybindings, done) =>
|
|
323
|
+
new GoalDashboard(tui, theme, keybindings, options, done),
|
|
324
|
+
{
|
|
325
|
+
overlay: true,
|
|
326
|
+
overlayOptions: {
|
|
327
|
+
anchor: "center",
|
|
328
|
+
width: "100%",
|
|
329
|
+
minWidth: 48,
|
|
330
|
+
maxHeight: "100%",
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Orchestration
|
|
2
|
+
|
|
3
|
+
Orchestration coordinates large, explicitly requested software tasks through one dedicated orchestrator and configurable specialist agents. It uses the suite's Subagents runtime rather than creating a competing execution system.
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
1. Open `/orchestration`, choose **New**, and describe the objective.
|
|
8
|
+
2. The dedicated orchestrator creates a dependency-aware plan and routes tasks to explorer, frontend, backend, or general workers.
|
|
9
|
+
3. Review the plan in the dashboard. Give feedback if needed, then approve it once.
|
|
10
|
+
4. Workers run autonomously. Git-backed writing tasks use one worktree per task; non-Git projects allow only one writer at a time.
|
|
11
|
+
5. The configured reviewer checks each task, may make small obvious fixes, and sends larger findings back to the same worker session with its existing context.
|
|
12
|
+
6. Reviewed changes integrate into a temporary orchestration branch. A final holistic review gates application to the source checkout.
|
|
13
|
+
7. The extension applies the reviewed result without committing or pushing.
|
|
14
|
+
|
|
15
|
+
The orchestrator sleeps while agents work and wakes automatically at execution boundaries. It asks the user only for a genuine blocker or scope-changing decision.
|
|
16
|
+
|
|
17
|
+
## Roles and settings
|
|
18
|
+
|
|
19
|
+
Use `/orchestration settings` to choose a global provider/model and thinking level for:
|
|
20
|
+
|
|
21
|
+
- orchestrator
|
|
22
|
+
- explorer
|
|
23
|
+
- frontend worker
|
|
24
|
+
- backend worker
|
|
25
|
+
- general worker
|
|
26
|
+
- reviewer
|
|
27
|
+
|
|
28
|
+
Settings are stored with restrictive permissions under Pi's active agent directory at `orchestration/settings.json`. They apply globally and are changed only through the settings UI.
|
|
29
|
+
|
|
30
|
+
Each project has up to 10 concurrent worker, explorer, or reviewer sessions. The dedicated orchestrator does not consume one of those slots. Multiple projects may run concurrently with independent pools.
|
|
31
|
+
|
|
32
|
+
## Dashboard
|
|
33
|
+
|
|
34
|
+
`/orchestration` opens the full-screen operations dashboard.
|
|
35
|
+
|
|
36
|
+
- `j` / `k`: select a run
|
|
37
|
+
- `Enter`: inspect tasks and reviews
|
|
38
|
+
- `n`: create a run
|
|
39
|
+
- `s`: configure role models
|
|
40
|
+
- `a`: approve the initial plan
|
|
41
|
+
- `f`: give plan feedback
|
|
42
|
+
- `p`: pause, resume, or recover
|
|
43
|
+
- `x`: cancel while preserving artifacts
|
|
44
|
+
- `Esc`: go back or close
|
|
45
|
+
|
|
46
|
+
Interrupted runs recover in a paused state and require explicit resume. Run state is machine-local under Pi's active agent directory at `orchestration/runs/<run-id>/`.
|
|
47
|
+
|
|
48
|
+
## Safety boundaries
|
|
49
|
+
|
|
50
|
+
- Orchestration starts only from `/orchestration` or an explicit natural-language request.
|
|
51
|
+
- A Git source checkout must be clean before writing begins.
|
|
52
|
+
- Writing workers never share a Git worktree.
|
|
53
|
+
- Reviewers make only small localized fixes; larger fixes return to the original worker session.
|
|
54
|
+
- A task gets at most two automatic fix rounds before the run blocks.
|
|
55
|
+
- Source changes integrate sequentially only after review.
|
|
56
|
+
- The extension never commits or pushes the source repository.
|
|
57
|
+
- Agent thinking is not copied into orchestration artifacts.
|
|
58
|
+
- Disabling Subagents makes Orchestration unavailable rather than falling back to another runtime.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
KeybindingsManager,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
Key,
|
|
7
|
+
matchesKey,
|
|
8
|
+
truncateToWidth,
|
|
9
|
+
visibleWidth,
|
|
10
|
+
type TUI,
|
|
11
|
+
} from "@earendil-works/pi-tui";
|
|
12
|
+
import type { OrchestrationEngine } from "./engine.ts";
|
|
13
|
+
import type { OrchestrationRun } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
interface DashboardActions {
|
|
16
|
+
create(): void;
|
|
17
|
+
settings(): void;
|
|
18
|
+
feedback(run: OrchestrationRun): void;
|
|
19
|
+
approve(run: OrchestrationRun): void;
|
|
20
|
+
togglePause(run: OrchestrationRun): void;
|
|
21
|
+
cancel(run: OrchestrationRun): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Theme = ExtensionContext["ui"]["theme"];
|
|
25
|
+
|
|
26
|
+
class OrchestrationDashboard {
|
|
27
|
+
private selected = 0;
|
|
28
|
+
private detail = false;
|
|
29
|
+
private unsubscribe: () => void;
|
|
30
|
+
private readonly tui: TUI;
|
|
31
|
+
private readonly theme: Theme;
|
|
32
|
+
private readonly keys: KeybindingsManager;
|
|
33
|
+
private readonly engine: OrchestrationEngine;
|
|
34
|
+
private readonly actions: DashboardActions;
|
|
35
|
+
private readonly close: () => void;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
tui: TUI,
|
|
39
|
+
theme: Theme,
|
|
40
|
+
keys: KeybindingsManager,
|
|
41
|
+
engine: OrchestrationEngine,
|
|
42
|
+
actions: DashboardActions,
|
|
43
|
+
close: () => void,
|
|
44
|
+
) {
|
|
45
|
+
this.tui = tui;
|
|
46
|
+
this.theme = theme;
|
|
47
|
+
this.keys = keys;
|
|
48
|
+
this.engine = engine;
|
|
49
|
+
this.actions = actions;
|
|
50
|
+
this.close = close;
|
|
51
|
+
this.unsubscribe = engineChangeSubscription(engine, () => tui.requestRender());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dispose() {
|
|
55
|
+
this.unsubscribe();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
invalidate() {}
|
|
59
|
+
|
|
60
|
+
handleInput(data: string) {
|
|
61
|
+
const runs = this.engine.list();
|
|
62
|
+
if (matchesKey(data, Key.escape)) {
|
|
63
|
+
if (this.detail) this.detail = false;
|
|
64
|
+
else {
|
|
65
|
+
this.dispose();
|
|
66
|
+
this.close();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
} else if (!this.detail && (this.keys.matches(data, "tui.select.up") || data === "k")) {
|
|
70
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
71
|
+
} else if (!this.detail && (this.keys.matches(data, "tui.select.down") || data === "j")) {
|
|
72
|
+
this.selected = Math.min(Math.max(0, runs.length - 1), this.selected + 1);
|
|
73
|
+
} else if (!this.detail && this.keys.matches(data, "tui.select.confirm") && runs[this.selected]) {
|
|
74
|
+
this.detail = true;
|
|
75
|
+
} else if (data === "n") this.actions.create();
|
|
76
|
+
else if (data === "s") this.actions.settings();
|
|
77
|
+
else if (this.detail && runs[this.selected]) {
|
|
78
|
+
const run = runs[this.selected];
|
|
79
|
+
if (data === "a" && run.status === "awaiting-approval") this.actions.approve(run);
|
|
80
|
+
else if (data === "f" && run.status === "awaiting-approval") this.actions.feedback(run);
|
|
81
|
+
else if (data === "p") this.actions.togglePause(run);
|
|
82
|
+
else if (data === "x") this.actions.cancel(run);
|
|
83
|
+
}
|
|
84
|
+
this.tui.requestRender();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
render(width: number) {
|
|
88
|
+
const runs = this.engine.list();
|
|
89
|
+
this.selected = Math.min(this.selected, Math.max(0, runs.length - 1));
|
|
90
|
+
const lines = this.detail && runs[this.selected]
|
|
91
|
+
? this.renderDetail(runs[this.selected], width)
|
|
92
|
+
: this.renderList(runs, width);
|
|
93
|
+
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private split(left: string, right: string, width: number) {
|
|
97
|
+
const rightWidth = visibleWidth(right);
|
|
98
|
+
const clipped = truncateToWidth(left, Math.max(0, width - rightWidth - 1), "…");
|
|
99
|
+
return clipped + " ".repeat(Math.max(1, width - visibleWidth(clipped) - rightWidth)) + right;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private renderList(runs: OrchestrationRun[], width: number) {
|
|
103
|
+
const lines = [
|
|
104
|
+
this.split(
|
|
105
|
+
this.theme.fg("accent", this.theme.bold("Orchestration")),
|
|
106
|
+
this.theme.fg("dim", `${runs.length} run${runs.length === 1 ? "" : "s"}`),
|
|
107
|
+
width,
|
|
108
|
+
),
|
|
109
|
+
this.theme.fg("borderMuted", "─".repeat(Math.max(0, width))),
|
|
110
|
+
];
|
|
111
|
+
if (!runs.length) lines.push(this.theme.fg("muted", "No runs yet. Press n to create one."));
|
|
112
|
+
for (const [index, run] of runs.entries()) {
|
|
113
|
+
const marker = index === this.selected ? this.theme.fg("accent", "❯") : " ";
|
|
114
|
+
const complete = run.tasks.filter((task) => task.status === "completed").length;
|
|
115
|
+
const color = run.status === "completed" ? "success" : run.status === "blocked" ? "error" : run.status === "running" ? "warning" : "muted";
|
|
116
|
+
lines.push(
|
|
117
|
+
this.split(
|
|
118
|
+
`${marker} ${this.theme.fg(color, "■")} ${this.theme.fg(index === this.selected ? "accent" : "text", run.objective.slice(0, 72))}`,
|
|
119
|
+
this.theme.fg("dim", `${complete}/${run.tasks.length} · ${run.status}`),
|
|
120
|
+
width,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
lines.push(` ${this.theme.fg("dim", `${run.id} · ${run.cwd}`)}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push("", this.theme.fg("dim", "j/k select · enter open · n new · s settings · esc close"));
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private renderDetail(run: OrchestrationRun, width: number) {
|
|
130
|
+
const lines = [
|
|
131
|
+
this.split(
|
|
132
|
+
this.theme.fg("accent", this.theme.bold(run.objective.slice(0, 100))),
|
|
133
|
+
this.theme.fg(run.status === "completed" ? "success" : run.status === "blocked" ? "error" : "warning", run.status),
|
|
134
|
+
width,
|
|
135
|
+
),
|
|
136
|
+
this.theme.fg("dim", `${run.id} · ${run.cwd}`),
|
|
137
|
+
this.theme.fg("borderMuted", "─".repeat(Math.max(0, width))),
|
|
138
|
+
this.theme.fg("muted", run.summary || "No summary yet."),
|
|
139
|
+
"",
|
|
140
|
+
this.theme.fg("accent", this.theme.bold("Tasks")),
|
|
141
|
+
];
|
|
142
|
+
for (const task of run.tasks) {
|
|
143
|
+
const color = task.status === "completed" ? "success" : task.status === "blocked" ? "error" : ["implementing", "reviewing", "integrating"].includes(task.status) ? "warning" : "muted";
|
|
144
|
+
lines.push(
|
|
145
|
+
this.split(
|
|
146
|
+
` ${this.theme.fg(color, "■")} ${this.theme.fg("text", task.title)} ${this.theme.fg("dim", `(${task.role})`)}`,
|
|
147
|
+
this.theme.fg(color, task.status),
|
|
148
|
+
width,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
if (task.dependencies.length) lines.push(` ${this.theme.fg("dim", `depends: ${task.dependencies.join(", ")}`)}`);
|
|
152
|
+
if (task.reviewSummary) lines.push(` ${this.theme.fg("muted", `review: ${task.reviewSummary.slice(0, 140)}`)}`);
|
|
153
|
+
if (task.error) lines.push(` ${this.theme.fg("error", task.error.slice(0, 180))}`);
|
|
154
|
+
}
|
|
155
|
+
if (run.finalReviewSummary) lines.push("", this.theme.fg("accent", `Final review: ${run.finalReviewSummary}`));
|
|
156
|
+
if (run.error) lines.push("", this.theme.fg("error", run.error));
|
|
157
|
+
const controls = [
|
|
158
|
+
run.status === "awaiting-approval" ? "a approve · f feedback" : "",
|
|
159
|
+
["paused", "interrupted", "blocked"].includes(run.status) ? "p resume" : run.status === "running" ? "p pause" : "",
|
|
160
|
+
!["completed", "cancelled"].includes(run.status) ? "x cancel" : "",
|
|
161
|
+
"esc back",
|
|
162
|
+
].filter(Boolean).join(" · ");
|
|
163
|
+
lines.push("", this.theme.fg("dim", controls));
|
|
164
|
+
return lines;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const listeners = new WeakMap<OrchestrationEngine, Set<() => void>>();
|
|
169
|
+
export function notifyDashboard(engine: OrchestrationEngine) {
|
|
170
|
+
for (const listener of listeners.get(engine) ?? []) listener();
|
|
171
|
+
}
|
|
172
|
+
function engineChangeSubscription(engine: OrchestrationEngine, listener: () => void) {
|
|
173
|
+
const set = listeners.get(engine) ?? new Set<() => void>();
|
|
174
|
+
set.add(listener);
|
|
175
|
+
listeners.set(engine, set);
|
|
176
|
+
return () => set.delete(listener);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function openOrchestrationDashboard(
|
|
180
|
+
ctx: ExtensionContext,
|
|
181
|
+
engine: OrchestrationEngine,
|
|
182
|
+
actions: DashboardActions,
|
|
183
|
+
) {
|
|
184
|
+
await ctx.ui.custom<void>(
|
|
185
|
+
(tui, theme, keys, done) =>
|
|
186
|
+
new OrchestrationDashboard(tui, theme, keys, engine, actions, () => done(undefined)),
|
|
187
|
+
{
|
|
188
|
+
overlay: true,
|
|
189
|
+
overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
}
|