shariq-pi-extensions 0.3.0 → 0.3.2
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.md +1 -4
- package/docs/ARCHITECTURE.md +2 -3
- package/docs/DEVELOPMENT.md +2 -2
- package/docs/EXTENSIONS.md +1 -15
- package/extensions/shared/README.md +1 -1
- package/extensions/shared/child-session.ts +0 -2
- package/extensions/shared/dashboard-state.ts +0 -44
- package/extensions/smart-compaction/engine.ts +83 -3
- package/extensions/subagents/src/backends/pi.ts +0 -2
- package/extensions/task-list/README.md +1 -1
- package/package.json +1 -5
- package/extensions/copy-all/README.md +0 -15
- package/extensions/copy-all/index.ts +0 -72
- package/extensions/git-info/README.md +0 -25
- package/extensions/git-info/index.ts +0 -160
- package/extensions/git-info/src/git.ts +0 -206
- package/extensions/git-info/src/ui.ts +0 -291
- package/extensions/orchestration/README.md +0 -58
- package/extensions/orchestration/dashboard.ts +0 -259
- package/extensions/orchestration/engine.ts +0 -706
- package/extensions/orchestration/index.ts +0 -279
- package/extensions/orchestration/prompts.ts +0 -124
- package/extensions/orchestration/protocol.ts +0 -116
- package/extensions/orchestration/settings-ui.ts +0 -213
- package/extensions/orchestration/settings.ts +0 -59
- package/extensions/orchestration/storage.ts +0 -49
- package/extensions/orchestration/types.ts +0 -105
- package/skills/orchestration/SKILL.md +0 -33
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { sanitizeTerminalText } from "../../shared/tui-dashboard.ts";
|
|
3
|
-
|
|
4
|
-
export interface GitSummary {
|
|
5
|
-
isRepository: boolean;
|
|
6
|
-
root?: string;
|
|
7
|
-
branch?: string;
|
|
8
|
-
changedFiles: number;
|
|
9
|
-
pullRequest?: { number: number; url: string; draft: boolean };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ChangedFile {
|
|
13
|
-
path: string;
|
|
14
|
-
name: string;
|
|
15
|
-
status: string;
|
|
16
|
-
additions: number | null;
|
|
17
|
-
deletions: number | null;
|
|
18
|
-
diff: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface ChangedPath {
|
|
22
|
-
path: string;
|
|
23
|
-
status: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const GIT_TIMEOUT_MS = 5_000;
|
|
27
|
-
const GH_TIMEOUT_MS = 10_000;
|
|
28
|
-
const MAX_CHANGED_FILES = 250;
|
|
29
|
-
const MAX_DIFF_LINES = 20_000;
|
|
30
|
-
|
|
31
|
-
function stripFinalLineBreak(value: string): string {
|
|
32
|
-
return value.replace(/\r?\n$/, "");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function safePath(value: string): string {
|
|
36
|
-
return sanitizeTerminalText(value).replace(/[\r\n\t]/g, " ");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseChangedPaths(output: string): ChangedPath[] {
|
|
40
|
-
const records = output.split("\0");
|
|
41
|
-
const paths: ChangedPath[] = [];
|
|
42
|
-
for (let index = 0; index < records.length; index++) {
|
|
43
|
-
const record = records[index];
|
|
44
|
-
if (!record || record.length < 4) continue;
|
|
45
|
-
const status = record.slice(0, 2);
|
|
46
|
-
const path = record.slice(3);
|
|
47
|
-
paths.push({ status, path });
|
|
48
|
-
if (status.includes("R") || status.includes("C")) index++;
|
|
49
|
-
}
|
|
50
|
-
return [...new Map(paths.map((entry) => [entry.path, entry])).values()];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function parseNumstat(output: string): {
|
|
54
|
-
additions: number | null;
|
|
55
|
-
deletions: number | null;
|
|
56
|
-
} {
|
|
57
|
-
const line = output.split("\n").find(Boolean);
|
|
58
|
-
if (!line) return { additions: 0, deletions: 0 };
|
|
59
|
-
const [added, deleted] = line.split("\t");
|
|
60
|
-
return {
|
|
61
|
-
additions: added === "-" ? null : Number.parseInt(added ?? "0", 10) || 0,
|
|
62
|
-
deletions: deleted === "-" ? null : Number.parseInt(deleted ?? "0", 10) || 0,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function loadGitSummary(
|
|
67
|
-
pi: ExtensionAPI,
|
|
68
|
-
cwd: string,
|
|
69
|
-
signal?: AbortSignal,
|
|
70
|
-
): Promise<GitSummary> {
|
|
71
|
-
const root = await pi.exec("git", ["rev-parse", "--show-toplevel"], {
|
|
72
|
-
cwd,
|
|
73
|
-
signal,
|
|
74
|
-
timeout: GIT_TIMEOUT_MS,
|
|
75
|
-
});
|
|
76
|
-
if (root.code !== 0) return { isRepository: false, changedFiles: 0 };
|
|
77
|
-
const repoRoot = stripFinalLineBreak(root.stdout);
|
|
78
|
-
const [branch, head, status] = await Promise.all([
|
|
79
|
-
pi.exec("git", ["branch", "--show-current"], {
|
|
80
|
-
cwd: repoRoot,
|
|
81
|
-
signal,
|
|
82
|
-
timeout: GIT_TIMEOUT_MS,
|
|
83
|
-
}),
|
|
84
|
-
pi.exec("git", ["rev-parse", "--short", "HEAD"], {
|
|
85
|
-
cwd: repoRoot,
|
|
86
|
-
signal,
|
|
87
|
-
timeout: GIT_TIMEOUT_MS,
|
|
88
|
-
}),
|
|
89
|
-
pi.exec("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
|
|
90
|
-
cwd: repoRoot,
|
|
91
|
-
signal,
|
|
92
|
-
timeout: GIT_TIMEOUT_MS,
|
|
93
|
-
}),
|
|
94
|
-
]);
|
|
95
|
-
const branchName = branch.stdout.trim();
|
|
96
|
-
const shortHead = head.stdout.trim();
|
|
97
|
-
return {
|
|
98
|
-
isRepository: true,
|
|
99
|
-
root: repoRoot,
|
|
100
|
-
branch: safePath(branchName || (shortHead ? `detached@${shortHead}` : "detached")),
|
|
101
|
-
changedFiles: status.code === 0 ? parseChangedPaths(status.stdout).length : 0,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function loadPullRequest(
|
|
106
|
-
pi: ExtensionAPI,
|
|
107
|
-
cwd: string,
|
|
108
|
-
signal?: AbortSignal,
|
|
109
|
-
): Promise<GitSummary["pullRequest"]> {
|
|
110
|
-
const result = await pi.exec(
|
|
111
|
-
"gh",
|
|
112
|
-
["pr", "view", "--json", "number,url,state,isDraft"],
|
|
113
|
-
{ cwd, signal, timeout: GH_TIMEOUT_MS },
|
|
114
|
-
);
|
|
115
|
-
if (result.code !== 0) return undefined;
|
|
116
|
-
try {
|
|
117
|
-
const value = JSON.parse(result.stdout) as unknown;
|
|
118
|
-
if (!value || typeof value !== "object") return undefined;
|
|
119
|
-
const record = value as Record<string, unknown>;
|
|
120
|
-
if (
|
|
121
|
-
typeof record.number !== "number" ||
|
|
122
|
-
typeof record.url !== "string" ||
|
|
123
|
-
record.state !== "OPEN"
|
|
124
|
-
) {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
number: record.number,
|
|
129
|
-
url: record.url,
|
|
130
|
-
draft: record.isDraft === true,
|
|
131
|
-
};
|
|
132
|
-
} catch {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function loadFile(
|
|
138
|
-
pi: ExtensionAPI,
|
|
139
|
-
repoRoot: string,
|
|
140
|
-
changed: ChangedPath,
|
|
141
|
-
hasHead: boolean,
|
|
142
|
-
signal?: AbortSignal,
|
|
143
|
-
): Promise<ChangedFile> {
|
|
144
|
-
const untracked = changed.status === "??" || !hasHead;
|
|
145
|
-
const diffArgs = untracked
|
|
146
|
-
? ["diff", "--no-index", "--no-ext-diff", "--no-color", "--unified=3", "--", "/dev/null", changed.path]
|
|
147
|
-
: ["diff", "--no-ext-diff", "--no-color", "--unified=3", "HEAD", "--", changed.path];
|
|
148
|
-
const statArgs = untracked
|
|
149
|
-
? ["diff", "--no-index", "--numstat", "--", "/dev/null", changed.path]
|
|
150
|
-
: ["diff", "--numstat", "HEAD", "--", changed.path];
|
|
151
|
-
const [diff, stat] = await Promise.all([
|
|
152
|
-
pi.exec("git", diffArgs, { cwd: repoRoot, signal, timeout: GH_TIMEOUT_MS }),
|
|
153
|
-
pi.exec("git", statArgs, { cwd: repoRoot, signal, timeout: GH_TIMEOUT_MS }),
|
|
154
|
-
]);
|
|
155
|
-
const allLines = diff.stdout
|
|
156
|
-
.trimEnd()
|
|
157
|
-
.split("\n")
|
|
158
|
-
.map(sanitizeTerminalText);
|
|
159
|
-
const lines = allLines.length > MAX_DIFF_LINES
|
|
160
|
-
? [...allLines.slice(0, MAX_DIFF_LINES), `… diff truncated after ${MAX_DIFF_LINES.toLocaleString()} lines …`]
|
|
161
|
-
: allLines;
|
|
162
|
-
const stats = parseNumstat(stat.stdout);
|
|
163
|
-
const path = safePath(changed.path);
|
|
164
|
-
return {
|
|
165
|
-
...stats,
|
|
166
|
-
path,
|
|
167
|
-
name: path.split("/").at(-1) || path,
|
|
168
|
-
status: sanitizeTerminalText(changed.status),
|
|
169
|
-
diff: lines.length === 1 && lines[0] === "" ? ["No textual diff available."] : lines,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export async function loadChangedFiles(
|
|
174
|
-
pi: ExtensionAPI,
|
|
175
|
-
cwd: string,
|
|
176
|
-
signal?: AbortSignal,
|
|
177
|
-
): Promise<{ root: string; files: ChangedFile[]; omitted: number } | null> {
|
|
178
|
-
const root = await pi.exec("git", ["rev-parse", "--show-toplevel"], {
|
|
179
|
-
cwd,
|
|
180
|
-
signal,
|
|
181
|
-
timeout: GIT_TIMEOUT_MS,
|
|
182
|
-
});
|
|
183
|
-
if (root.code !== 0) return null;
|
|
184
|
-
const repoRoot = stripFinalLineBreak(root.stdout);
|
|
185
|
-
const [status, head] = await Promise.all([
|
|
186
|
-
pi.exec("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
|
|
187
|
-
cwd: repoRoot,
|
|
188
|
-
signal,
|
|
189
|
-
timeout: GIT_TIMEOUT_MS,
|
|
190
|
-
}),
|
|
191
|
-
pi.exec("git", ["rev-parse", "--verify", "HEAD"], {
|
|
192
|
-
cwd: repoRoot,
|
|
193
|
-
signal,
|
|
194
|
-
timeout: GIT_TIMEOUT_MS,
|
|
195
|
-
}),
|
|
196
|
-
]);
|
|
197
|
-
if (status.code !== 0) return null;
|
|
198
|
-
const changed = parseChangedPaths(status.stdout);
|
|
199
|
-
const selected = changed.slice(0, MAX_CHANGED_FILES);
|
|
200
|
-
const files: ChangedFile[] = [];
|
|
201
|
-
for (const entry of selected) {
|
|
202
|
-
signal?.throwIfAborted();
|
|
203
|
-
files.push(await loadFile(pi, repoRoot, entry, head.code === 0, signal));
|
|
204
|
-
}
|
|
205
|
-
return { root: repoRoot, files, omitted: changed.length - selected.length };
|
|
206
|
-
}
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionContext,
|
|
3
|
-
Theme,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
Key,
|
|
7
|
-
matchesKey,
|
|
8
|
-
truncateToWidth,
|
|
9
|
-
} from "@earendil-works/pi-tui";
|
|
10
|
-
import {
|
|
11
|
-
frameBottom,
|
|
12
|
-
frameTop,
|
|
13
|
-
joinSides,
|
|
14
|
-
oneLine,
|
|
15
|
-
padLine,
|
|
16
|
-
stateLabel,
|
|
17
|
-
viewportSlice,
|
|
18
|
-
} from "../../shared/tui-dashboard.ts";
|
|
19
|
-
import type { ChangedFile } from "./git.ts";
|
|
20
|
-
|
|
21
|
-
function statusState(status: string): "success" | "warning" | "error" {
|
|
22
|
-
if (status === "??" || status.includes("A")) return "success";
|
|
23
|
-
if (status.includes("D")) return "error";
|
|
24
|
-
return "warning";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function statText(file: ChangedFile): string {
|
|
28
|
-
if (file.additions === null || file.deletions === null) return "binary";
|
|
29
|
-
return `+${file.additions} -${file.deletions}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function styleDiff(theme: Theme, line: string): string {
|
|
33
|
-
if (line.startsWith("diff --git") || line.startsWith("index ")) {
|
|
34
|
-
return theme.fg("accent", theme.bold(line));
|
|
35
|
-
}
|
|
36
|
-
if (line.startsWith("@@")) return theme.fg("mdHeading", line);
|
|
37
|
-
if (line.startsWith("---") || line.startsWith("+++")) return theme.fg("muted", line);
|
|
38
|
-
if (line.startsWith("+")) return theme.fg("success", line);
|
|
39
|
-
if (line.startsWith("-")) return theme.fg("error", line);
|
|
40
|
-
if (line.startsWith("…")) return theme.fg("warning", line);
|
|
41
|
-
return theme.fg("text", line);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function combine(left: string, right: string, width: number): string {
|
|
45
|
-
return truncateToWidth(`${left}${right}`, width, "");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class GitChangesView {
|
|
49
|
-
private selected = 0;
|
|
50
|
-
private focus: "files" | "diff" = "files";
|
|
51
|
-
private diffOffset = 0;
|
|
52
|
-
private cachedWidth?: number;
|
|
53
|
-
private cachedHeight?: number;
|
|
54
|
-
private cachedLines?: string[];
|
|
55
|
-
private readonly files: ReadonlyArray<ChangedFile>;
|
|
56
|
-
private readonly root: string;
|
|
57
|
-
private readonly omitted: number;
|
|
58
|
-
private readonly theme: Theme;
|
|
59
|
-
private readonly rows: () => number;
|
|
60
|
-
private readonly requestRender: () => void;
|
|
61
|
-
private readonly close: () => void;
|
|
62
|
-
|
|
63
|
-
constructor(
|
|
64
|
-
files: ReadonlyArray<ChangedFile>,
|
|
65
|
-
root: string,
|
|
66
|
-
omitted: number,
|
|
67
|
-
theme: Theme,
|
|
68
|
-
rows: () => number,
|
|
69
|
-
requestRender: () => void,
|
|
70
|
-
close: () => void,
|
|
71
|
-
) {
|
|
72
|
-
this.files = files;
|
|
73
|
-
this.root = root;
|
|
74
|
-
this.omitted = omitted;
|
|
75
|
-
this.theme = theme;
|
|
76
|
-
this.rows = rows;
|
|
77
|
-
this.requestRender = requestRender;
|
|
78
|
-
this.close = close;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private current(): ChangedFile {
|
|
82
|
-
return this.files[this.selected]!;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private invalidateRender(): void {
|
|
86
|
-
this.cachedWidth = undefined;
|
|
87
|
-
this.cachedHeight = undefined;
|
|
88
|
-
this.cachedLines = undefined;
|
|
89
|
-
this.requestRender();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private moveFile(amount: number): void {
|
|
93
|
-
this.selected = (this.selected + amount + this.files.length) % this.files.length;
|
|
94
|
-
this.diffOffset = 0;
|
|
95
|
-
this.invalidateRender();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private moveDiff(amount: number): void {
|
|
99
|
-
const bodyHeight = Math.max(1, this.rows() - 8);
|
|
100
|
-
const max = Math.max(0, this.current().diff.length - bodyHeight);
|
|
101
|
-
this.diffOffset = Math.max(0, Math.min(max, this.diffOffset + amount));
|
|
102
|
-
this.invalidateRender();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
handleInput(data: string): void {
|
|
106
|
-
if (this.focus === "files") {
|
|
107
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
108
|
-
this.close();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (matchesKey(data, Key.down) || data === "j") this.moveFile(1);
|
|
112
|
-
else if (matchesKey(data, Key.up) || data === "k") this.moveFile(-1);
|
|
113
|
-
else if (matchesKey(data, Key.home) || data === "g") {
|
|
114
|
-
this.selected = 0;
|
|
115
|
-
this.diffOffset = 0;
|
|
116
|
-
this.invalidateRender();
|
|
117
|
-
} else if (matchesKey(data, Key.end) || data === "G") {
|
|
118
|
-
this.selected = this.files.length - 1;
|
|
119
|
-
this.diffOffset = 0;
|
|
120
|
-
this.invalidateRender();
|
|
121
|
-
} else if (
|
|
122
|
-
matchesKey(data, Key.enter) ||
|
|
123
|
-
matchesKey(data, Key.tab) ||
|
|
124
|
-
matchesKey(data, Key.right) ||
|
|
125
|
-
data === "l"
|
|
126
|
-
) {
|
|
127
|
-
this.focus = "diff";
|
|
128
|
-
this.invalidateRender();
|
|
129
|
-
}
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
matchesKey(data, Key.escape) ||
|
|
135
|
-
matchesKey(data, Key.tab) ||
|
|
136
|
-
matchesKey(data, Key.left) ||
|
|
137
|
-
data === "h"
|
|
138
|
-
) {
|
|
139
|
-
this.focus = "files";
|
|
140
|
-
this.invalidateRender();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (matchesKey(data, Key.down) || data === "j") this.moveDiff(4);
|
|
144
|
-
else if (matchesKey(data, Key.up) || data === "k") this.moveDiff(-4);
|
|
145
|
-
else if (matchesKey(data, Key.ctrl("d")) || matchesKey(data, Key.pageDown)) {
|
|
146
|
-
this.moveDiff(Math.max(1, Math.floor((this.rows() - 8) / 2)));
|
|
147
|
-
} else if (matchesKey(data, Key.ctrl("u")) || matchesKey(data, Key.pageUp)) {
|
|
148
|
-
this.moveDiff(-Math.max(1, Math.floor((this.rows() - 8) / 2)));
|
|
149
|
-
} else if (matchesKey(data, Key.home) || data === "g") {
|
|
150
|
-
this.diffOffset = 0;
|
|
151
|
-
this.invalidateRender();
|
|
152
|
-
} else if (matchesKey(data, Key.end) || data === "G") {
|
|
153
|
-
this.diffOffset = Math.max(0, this.current().diff.length - Math.max(1, this.rows() - 8));
|
|
154
|
-
this.invalidateRender();
|
|
155
|
-
} else if (matchesKey(data, Key.ctrl("c"))) {
|
|
156
|
-
this.close();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private renderList(width: number, height: number): string[] {
|
|
161
|
-
const inner = Math.max(1, width - 2);
|
|
162
|
-
const viewport = viewportSlice(this.files, this.selected, height);
|
|
163
|
-
const lines: string[] = [];
|
|
164
|
-
for (let row = 0; row < height; row++) {
|
|
165
|
-
const file = viewport.items[row];
|
|
166
|
-
if (!file) {
|
|
167
|
-
lines.push(`${this.theme.fg("border", "│")}${padLine("", inner)}${this.theme.fg("border", "│")}`);
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
const index = viewport.start + row;
|
|
171
|
-
const selected = index === this.selected;
|
|
172
|
-
const prefix = selected ? "❯" : " ";
|
|
173
|
-
const left = `${prefix} ${file.status.padEnd(2)} ${oneLine(file.path)}`;
|
|
174
|
-
const rowText = joinSides(left, statText(file), inner);
|
|
175
|
-
const styled = selected
|
|
176
|
-
? this.theme.bg("selectedBg", this.theme.fg("accent", rowText))
|
|
177
|
-
: this.theme.fg(statusState(file.status), rowText);
|
|
178
|
-
lines.push(`${this.theme.fg(selected ? "borderAccent" : "border", "│")}${padLine(styled, inner)}${this.theme.fg(selected ? "borderAccent" : "border", "│")}`);
|
|
179
|
-
}
|
|
180
|
-
return lines;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private renderDiff(width: number, height: number): string[] {
|
|
184
|
-
const file = this.current();
|
|
185
|
-
const inner = Math.max(1, width - 2);
|
|
186
|
-
const lines: string[] = [];
|
|
187
|
-
const max = Math.max(0, file.diff.length - height);
|
|
188
|
-
this.diffOffset = Math.min(this.diffOffset, max);
|
|
189
|
-
for (let row = 0; row < height; row++) {
|
|
190
|
-
const source = file.diff[this.diffOffset + row];
|
|
191
|
-
const content = source === undefined ? "" : styleDiff(this.theme, source.replaceAll("\t", " "));
|
|
192
|
-
lines.push(`${this.theme.fg(this.focus === "diff" ? "borderAccent" : "border", "│")}${padLine(content, inner)}${this.theme.fg(this.focus === "diff" ? "borderAccent" : "border", "│")}`);
|
|
193
|
-
}
|
|
194
|
-
return lines;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
render(width: number): string[] {
|
|
198
|
-
const terminalRows = this.rows();
|
|
199
|
-
if (
|
|
200
|
-
this.cachedLines &&
|
|
201
|
-
this.cachedWidth === width &&
|
|
202
|
-
this.cachedHeight === terminalRows
|
|
203
|
-
) {
|
|
204
|
-
return this.cachedLines;
|
|
205
|
-
}
|
|
206
|
-
const safeWidth = Math.max(1, width);
|
|
207
|
-
const bodyHeight = Math.max(4, terminalRows - 7);
|
|
208
|
-
const current = this.current();
|
|
209
|
-
const totals = this.files.reduce(
|
|
210
|
-
(sum, file) => ({
|
|
211
|
-
additions: sum.additions + (file.additions ?? 0),
|
|
212
|
-
deletions: sum.deletions + (file.deletions ?? 0),
|
|
213
|
-
}),
|
|
214
|
-
{ additions: 0, deletions: 0 },
|
|
215
|
-
);
|
|
216
|
-
const loaded = this.omitted > 0
|
|
217
|
-
? `${this.files.length} shown · ${this.omitted} omitted`
|
|
218
|
-
: `${this.files.length} changed`;
|
|
219
|
-
const header = joinSides(
|
|
220
|
-
` ${this.theme.bold("Git changes")} ${this.theme.fg("muted", oneLine(this.root))}`,
|
|
221
|
-
`${stateLabel(this.theme, "success", `+${totals.additions}`)} ${stateLabel(this.theme, "error", `-${totals.deletions}`)}`,
|
|
222
|
-
safeWidth,
|
|
223
|
-
);
|
|
224
|
-
const lines = [header];
|
|
225
|
-
|
|
226
|
-
if (safeWidth >= 88) {
|
|
227
|
-
const leftWidth = Math.max(34, Math.floor(safeWidth * 0.4));
|
|
228
|
-
const rightWidth = safeWidth - leftWidth;
|
|
229
|
-
const listTitle = `${loaded}${this.focus === "files" ? " · active" : ""}`;
|
|
230
|
-
const diffTitle = `${oneLine(current.path)} · ${statText(current)}${this.focus === "diff" ? " · active" : ""}`;
|
|
231
|
-
lines.push(combine(frameTop(this.theme, leftWidth, listTitle), frameTop(this.theme, rightWidth, diffTitle), safeWidth));
|
|
232
|
-
const list = this.renderList(leftWidth, bodyHeight);
|
|
233
|
-
const diff = this.renderDiff(rightWidth, bodyHeight);
|
|
234
|
-
for (let row = 0; row < bodyHeight; row++) {
|
|
235
|
-
lines.push(combine(list[row]!, diff[row]!, safeWidth));
|
|
236
|
-
}
|
|
237
|
-
lines.push(combine(frameBottom(this.theme, leftWidth), frameBottom(this.theme, rightWidth), safeWidth));
|
|
238
|
-
} else if (this.focus === "files") {
|
|
239
|
-
lines.push(frameTop(this.theme, safeWidth, loaded));
|
|
240
|
-
lines.push(...this.renderList(safeWidth, bodyHeight));
|
|
241
|
-
lines.push(frameBottom(this.theme, safeWidth));
|
|
242
|
-
} else {
|
|
243
|
-
lines.push(frameTop(this.theme, safeWidth, `${oneLine(current.path)} · ${statText(current)}`));
|
|
244
|
-
lines.push(...this.renderDiff(safeWidth, bodyHeight));
|
|
245
|
-
lines.push(frameBottom(this.theme, safeWidth));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const hint = this.focus === "files"
|
|
249
|
-
? " up/down/jk select · enter/tab inspect · g/G first/last · escape close"
|
|
250
|
-
: " up/down/jk scroll · ctrl+u/d page · tab/h files · ctrl+c close";
|
|
251
|
-
lines.push(padLine(this.theme.fg("dim", hint), safeWidth));
|
|
252
|
-
this.cachedWidth = width;
|
|
253
|
-
this.cachedHeight = terminalRows;
|
|
254
|
-
this.cachedLines = lines.map((line) => truncateToWidth(line, safeWidth, ""));
|
|
255
|
-
return this.cachedLines;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
invalidate(): void {
|
|
259
|
-
this.cachedWidth = undefined;
|
|
260
|
-
this.cachedHeight = undefined;
|
|
261
|
-
this.cachedLines = undefined;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export async function openGitChanges(
|
|
266
|
-
ctx: ExtensionContext,
|
|
267
|
-
root: string,
|
|
268
|
-
files: ReadonlyArray<ChangedFile>,
|
|
269
|
-
omitted: number,
|
|
270
|
-
): Promise<void> {
|
|
271
|
-
await ctx.ui.custom<void>(
|
|
272
|
-
(tui, theme, _keybindings, done) =>
|
|
273
|
-
new GitChangesView(
|
|
274
|
-
files,
|
|
275
|
-
root,
|
|
276
|
-
omitted,
|
|
277
|
-
theme,
|
|
278
|
-
() => tui.terminal.rows || 30,
|
|
279
|
-
() => tui.requestRender(),
|
|
280
|
-
() => done(undefined),
|
|
281
|
-
),
|
|
282
|
-
{
|
|
283
|
-
overlay: true,
|
|
284
|
-
overlayOptions: {
|
|
285
|
-
width: "100%",
|
|
286
|
-
maxHeight: "100%",
|
|
287
|
-
minWidth: 40,
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
);
|
|
291
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
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` (or `s` in the dashboard) to open the interactive role configuration panel. Navigate roles with `j`/`k`, press `Enter` to search and select models from all active providers in the Searchable Model Picker, press `t` or `Tab` to cycle thinking levels directly, and press `s` to save. Settings configure:
|
|
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 creation and inspection render as compact main-chat cards with expandable detail. Active runs appear in the shared bounded **Active work** dock; plan-ready and blocked runs are prioritized as attention states. `/orchestration` opens a full-height framed operations dashboard with a bounded body and stable selection. Dashboard actions close the overlay before opening editors, settings, or confirmation prompts, then return to the live dashboard so nested UI cannot stall. **New** opens the objective editor before asking for missing role models, so cancelling a draft does not force configuration; cancelling unchanged settings does not claim they were saved.
|
|
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`: leave the dashboard and confirm cancellation 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.
|