-
Notifications
You must be signed in to change notification settings - Fork 79
Expand file tree
/
Copy pathagent-manager-chain-detail.ts
More file actions
116 lines (104 loc) · 4.91 KB
/
agent-manager-chain-detail.ts
File metadata and controls
116 lines (104 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import type { Theme } from "@mariozechner/pi-coding-agent";
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
import type { ChainConfig, ChainStepConfig } from "./agents.js";
import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "./render-helpers.js";
export interface ChainDetailState {
scrollOffset: number;
}
export type ChainDetailAction =
| { type: "back" }
| { type: "launch" }
| { type: "edit" };
const CHAIN_DETAIL_VIEWPORT_HEIGHT = 12;
export function buildDependencyMap(steps: ChainStepConfig[]): Map<number, number[]> {
const outputMap = new Map<string, number>();
const deps = new Map<number, number[]>();
for (let i = 0; i < steps.length; i++) {
const step = steps[i]!;
if (typeof step.output === "string" && step.output.length > 0) outputMap.set(step.output, i);
if (Array.isArray(step.reads) && step.reads.length > 0) {
const sources = step.reads
.map((file) => outputMap.get(file))
.filter((idx): idx is number => idx !== undefined);
if (sources.length > 0) deps.set(i, sources);
}
}
return deps;
}
function buildChainDetailLines(chain: ChainConfig, width: number): string[] {
const contentWidth = width - 3;
const lines: string[] = [];
const dependencyMap = buildDependencyMap(chain.steps);
lines.push(truncateToWidth(chain.description, contentWidth));
lines.push("");
lines.push(truncateToWidth(`File: ${formatPath(chain.filePath)}`, contentWidth));
lines.push("");
lines.push(truncateToWidth("── Flow ──", contentWidth));
for (let i = 0; i < chain.steps.length; i++) {
const step = chain.steps[i]!;
lines.push(truncateToWidth(` ${i + 1} ${step.agent}`, contentWidth));
const taskPreview = step.task.split("\n")[0] ?? "";
lines.push(truncateToWidth(` task: ${taskPreview || "(none)"}`, contentWidth));
if (Array.isArray(step.reads) && step.reads.length > 0) {
const sources = dependencyMap.get(i);
const fromText = sources && sources.length > 0 ? ` (from ${sources.map((s) => s + 1).join(", ")})` : "";
lines.push(truncateToWidth(` ← reads: ${step.reads.join(", ")}${fromText}`, contentWidth));
} else if (step.reads === false) {
lines.push(truncateToWidth(" ← reads: (disabled)", contentWidth));
}
if (typeof step.output === "string" && step.output.length > 0) {
lines.push(truncateToWidth(` → output: ${step.output}`, contentWidth));
} else if (step.output === false) {
lines.push(truncateToWidth(" → output: (disabled)", contentWidth));
}
if (step.model) lines.push(truncateToWidth(` model: ${step.model}`, contentWidth));
if (step.skills !== undefined) {
const skillsText =
step.skills === false
? "(disabled)"
: step.skills.length > 0
? step.skills.join(", ")
: "(none)";
lines.push(truncateToWidth(` skills: ${skillsText}`, contentWidth));
}
if (step.progress !== undefined) {
lines.push(truncateToWidth(` progress: ${step.progress ? "on" : "off"}`, contentWidth));
}
lines.push("");
}
if (chain.steps.length === 0) {
lines.push(truncateToWidth("(no steps)", contentWidth));
}
return lines;
}
export function handleChainDetailInput(state: ChainDetailState, data: string): ChainDetailAction | undefined {
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { type: "back" };
if (data === "l") return { type: "launch" };
if (data === "e") return { type: "edit" };
if (matchesKey(data, "up")) { state.scrollOffset--; return; }
if (matchesKey(data, "down")) { state.scrollOffset++; return; }
if (matchesKey(data, "pageup") || matchesKey(data, "shift+up")) { state.scrollOffset -= CHAIN_DETAIL_VIEWPORT_HEIGHT; return; }
if (matchesKey(data, "pagedown") || matchesKey(data, "shift+down")) { state.scrollOffset += CHAIN_DETAIL_VIEWPORT_HEIGHT; return; }
return;
}
export function renderChainDetail(
state: ChainDetailState,
chain: ChainConfig,
width: number,
theme: Theme,
): string[] {
const lines: string[] = [];
const scopeBadge = chain.source === "user" ? "[user]" : "[proj]";
lines.push(renderHeader(` ${chain.name} [chain] ${scopeBadge} `, width, theme));
lines.push(row("", width, theme));
const contentLines = buildChainDetailLines(chain, width);
const maxOffset = Math.max(0, contentLines.length - CHAIN_DETAIL_VIEWPORT_HEIGHT);
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset, maxOffset));
const visible = contentLines.slice(state.scrollOffset, state.scrollOffset + CHAIN_DETAIL_VIEWPORT_HEIGHT);
for (const line of visible) lines.push(row(` ${line}`, width, theme));
for (let i = visible.length; i < CHAIN_DETAIL_VIEWPORT_HEIGHT; i++) lines.push(row("", width, theme));
const scrollInfo = formatScrollInfo(state.scrollOffset, Math.max(0, contentLines.length - (state.scrollOffset + CHAIN_DETAIL_VIEWPORT_HEIGHT)));
lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
lines.push(renderFooter(" [l]aunch [e]dit [↑↓] scroll [esc] back ", width, theme));
return lines;
}