pi-subagents 0.23.1 → 0.24.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/CHANGELOG.md +13 -0
- package/README.md +13 -76
- package/package.json +1 -1
- package/prompts/parallel-cleanup.md +11 -1
- package/prompts/parallel-review.md +11 -1
- package/skills/pi-subagents/SKILL.md +11 -12
- package/src/agents/agent-serializer.ts +0 -42
- package/src/agents/agents.ts +1 -1
- package/src/extension/index.ts +2 -2
- package/src/runs/background/async-status.ts +16 -50
- package/src/runs/background/run-status.ts +8 -9
- package/src/runs/foreground/chain-clarify.ts +183 -218
- package/src/shared/status-format.ts +49 -0
- package/src/shared/types.ts +0 -5
- package/src/slash/slash-commands.ts +0 -74
- package/src/tui/render.ts +32 -58
- package/src/agents/agent-templates.ts +0 -60
- package/src/manager-ui/agent-manager-chain-detail.ts +0 -164
- package/src/manager-ui/agent-manager-detail.ts +0 -235
- package/src/manager-ui/agent-manager-edit.ts +0 -456
- package/src/manager-ui/agent-manager-list.ts +0 -283
- package/src/manager-ui/agent-manager-parallel.ts +0 -302
- package/src/manager-ui/agent-manager.ts +0 -732
- package/src/tui/subagents-status.ts +0 -621
- package/src/tui/text-editor.ts +0 -286
package/src/tui/render.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from
|
|
|
9
9
|
import {
|
|
10
10
|
type AgentProgress,
|
|
11
11
|
type AsyncJobState,
|
|
12
|
+
type AsyncJobStep,
|
|
13
|
+
type AsyncParallelGroupStatus,
|
|
12
14
|
type Details,
|
|
13
15
|
MAX_WIDGET_JOBS,
|
|
14
16
|
WIDGET_KEY,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
17
19
|
import { getDisplayItems, getLastActivity, getSingleResultOutput } from "../shared/utils.ts";
|
|
18
20
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
21
|
+
import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
|
|
19
22
|
|
|
20
23
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
21
24
|
|
|
@@ -166,23 +169,6 @@ function getToolCallLines(
|
|
|
166
169
|
return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
function formatActivityAge(ms: number): string {
|
|
170
|
-
if (ms < 1000) return "now";
|
|
171
|
-
if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
|
|
172
|
-
return `${Math.floor(ms / 60000)}m`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function formatActivityLabel(lastActivityAt: number | undefined, activityState?: AgentProgress["activityState"], now = Date.now()): string | undefined {
|
|
176
|
-
if (lastActivityAt === undefined) {
|
|
177
|
-
if (activityState === "needs_attention") return "needs attention";
|
|
178
|
-
if (activityState === "active_long_running") return "active but long-running";
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
const age = formatActivityAge(Math.max(0, now - lastActivityAt));
|
|
182
|
-
if (activityState === "needs_attention") return `no activity for ${age}`;
|
|
183
|
-
if (activityState === "active_long_running") return `active but long-running · last activity ${age} ago`;
|
|
184
|
-
return age === "now" ? "active now" : `active ${age} ago`;
|
|
185
|
-
}
|
|
186
172
|
|
|
187
173
|
function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">, availableWidth: number, expanded: boolean): string | undefined {
|
|
188
174
|
if (!progress.currentTool) return undefined;
|
|
@@ -310,7 +296,7 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
310
296
|
return theme.fg("error", "✗");
|
|
311
297
|
}
|
|
312
298
|
|
|
313
|
-
function widgetStepGlyph(status:
|
|
299
|
+
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme): string {
|
|
314
300
|
if (status === "running") return theme.fg("accent", spinnerFrame());
|
|
315
301
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
316
302
|
if (status === "failed") return theme.fg("error", "✗");
|
|
@@ -318,7 +304,7 @@ function widgetStepGlyph(status: string, theme: Theme): string {
|
|
|
318
304
|
return theme.fg("muted", "◦");
|
|
319
305
|
}
|
|
320
306
|
|
|
321
|
-
function widgetStepStatus(status:
|
|
307
|
+
function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
|
|
322
308
|
if (status === "running") return theme.fg("accent", "running");
|
|
323
309
|
if (status === "complete" || status === "completed") return theme.fg("success", "complete");
|
|
324
310
|
if (status === "failed") return theme.fg("error", "failed");
|
|
@@ -340,48 +326,24 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number]):
|
|
|
340
326
|
return facts.join(" · ");
|
|
341
327
|
}
|
|
342
328
|
|
|
343
|
-
function widgetAggregateStepStatus(steps: NonNullable<AsyncJobState["steps"]>): string {
|
|
344
|
-
if (steps.some((step) => step.status === "running")) return "running";
|
|
345
|
-
if (steps.some((step) => step.status === "failed")) return "failed";
|
|
346
|
-
if (steps.some((step) => step.status === "paused")) return "paused";
|
|
347
|
-
if (steps.length > 0 && steps.every((step) => step.status === "complete" || step.status === "completed")) return "complete";
|
|
348
|
-
return "pending";
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function widgetParallelOutcome(steps: NonNullable<AsyncJobState["steps"]>, total: number): string {
|
|
352
|
-
const running = steps.filter((step) => step.status === "running").length;
|
|
353
|
-
const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
354
|
-
const failed = steps.filter((step) => step.status === "failed").length;
|
|
355
|
-
const paused = steps.filter((step) => step.status === "paused").length;
|
|
356
|
-
const parts = [`${done}/${total} done`];
|
|
357
|
-
if (running > 0) parts.unshift(formatAgentRunningLabel(running));
|
|
358
|
-
if (failed > 0) parts.push(`${failed} failed`);
|
|
359
|
-
if (paused > 0) parts.push(`${paused} paused`);
|
|
360
|
-
return parts.join(" · ");
|
|
361
|
-
}
|
|
362
329
|
|
|
363
330
|
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
364
331
|
if (!job.steps?.length) return [];
|
|
365
332
|
const total = job.chainStepCount ?? job.steps.length;
|
|
366
|
-
const groups = job.parallelGroups ?? [];
|
|
367
333
|
const lines: string[] = [];
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const status = widgetAggregateStepStatus(steps);
|
|
374
|
-
lines.push(` ${widgetStepGlyph(status, theme)} Step ${stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", widgetParallelOutcome(steps, group.count))}`);
|
|
375
|
-
flatIndex = Math.max(flatIndex, group.start + group.count);
|
|
334
|
+
for (const span of buildAsyncChainStepSpans(total, job.steps.length, job.parallelGroups)) {
|
|
335
|
+
const steps = job.steps.slice(span.start, span.start + span.count);
|
|
336
|
+
if (span.isParallel) {
|
|
337
|
+
const status = aggregateStepStatus(steps);
|
|
338
|
+
lines.push(` ${widgetStepGlyph(status, theme)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
376
339
|
continue;
|
|
377
340
|
}
|
|
378
|
-
const step =
|
|
341
|
+
const step = steps[0];
|
|
379
342
|
if (!step) {
|
|
380
|
-
lines.push(` ${theme.fg("dim", `◦ Step ${stepIndex + 1}/${total}: pending`)}`);
|
|
343
|
+
lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
|
|
381
344
|
continue;
|
|
382
345
|
}
|
|
383
|
-
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", stepIndex + 1, total, expanded, width));
|
|
384
|
-
flatIndex++;
|
|
346
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width));
|
|
385
347
|
}
|
|
386
348
|
return lines;
|
|
387
349
|
}
|
|
@@ -434,8 +396,20 @@ function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[]
|
|
|
434
396
|
return spans;
|
|
435
397
|
}
|
|
436
398
|
|
|
437
|
-
function
|
|
438
|
-
|
|
399
|
+
function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
|
|
400
|
+
const spans: ChainStepSpan[] = [];
|
|
401
|
+
let flatIndex = 0;
|
|
402
|
+
for (let stepIndex = 0; stepIndex < total; stepIndex++) {
|
|
403
|
+
const group = parallelGroups.find((candidate) => candidate.stepIndex === stepIndex);
|
|
404
|
+
if (group) {
|
|
405
|
+
spans.push({ stepIndex, start: group.start, count: group.count, isParallel: true });
|
|
406
|
+
flatIndex = Math.max(flatIndex, group.start + group.count);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
spans.push({ stepIndex, start: flatIndex, count: flatIndex < stepCount ? 1 : 0, isParallel: false });
|
|
410
|
+
flatIndex++;
|
|
411
|
+
}
|
|
412
|
+
return spans;
|
|
439
413
|
}
|
|
440
414
|
|
|
441
415
|
function isDoneResult(result: Details["results"][number]): boolean {
|
|
@@ -670,7 +644,7 @@ function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number,
|
|
|
670
644
|
const mode = widgetJobName(job);
|
|
671
645
|
const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
|
|
672
646
|
return [
|
|
673
|
-
`${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background
|
|
647
|
+
`${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
|
|
674
648
|
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
675
649
|
...foregroundStyleWidgetDetails(job, theme, expanded, width),
|
|
676
650
|
].map((line) => truncLine(line, width));
|
|
@@ -690,7 +664,7 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
|
|
|
690
664
|
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
691
665
|
lines.push(` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
692
666
|
}
|
|
693
|
-
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail
|
|
667
|
+
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
|
|
694
668
|
return lines.map((line) => truncLine(line, width));
|
|
695
669
|
}
|
|
696
670
|
|
|
@@ -703,8 +677,8 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
|
|
|
703
677
|
const visibleLines = Math.max(1, budget - 1);
|
|
704
678
|
const hiddenCount = lines.length - visibleLines;
|
|
705
679
|
const hint = expanded
|
|
706
|
-
? `… ${hiddenCount} live-detail lines hidden
|
|
707
|
-
: `… ${hiddenCount} lines hidden · Ctrl+O expands
|
|
680
|
+
? `… ${hiddenCount} live-detail lines hidden`
|
|
681
|
+
: `… ${hiddenCount} lines hidden · Ctrl+O expands`;
|
|
708
682
|
return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
|
|
709
683
|
}
|
|
710
684
|
|
|
@@ -731,7 +705,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
731
705
|
|
|
732
706
|
const lines: string[] = [];
|
|
733
707
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
734
|
-
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", hasActive ? "●" : "○")} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background
|
|
708
|
+
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", hasActive ? "●" : "○")} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
|
|
735
709
|
|
|
736
710
|
const items: string[][] = [];
|
|
737
711
|
let hiddenRunning = 0;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { AgentConfig } from "./agents.ts";
|
|
2
|
-
|
|
3
|
-
export interface AgentTemplate {
|
|
4
|
-
name: string;
|
|
5
|
-
config: Partial<AgentConfig>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type TemplateItem =
|
|
9
|
-
| { type: "agent"; name: string; config: Partial<AgentConfig> }
|
|
10
|
-
| { type: "chain"; name: string; description: string }
|
|
11
|
-
| { type: "separator"; label: string };
|
|
12
|
-
|
|
13
|
-
export const TEMPLATE_ITEMS: TemplateItem[] = [
|
|
14
|
-
{ type: "separator", label: "Agents" },
|
|
15
|
-
{
|
|
16
|
-
type: "agent",
|
|
17
|
-
name: "Blank",
|
|
18
|
-
config: { description: "Describe this agent", systemPrompt: "" },
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
type: "agent",
|
|
22
|
-
name: "Scout",
|
|
23
|
-
config: {
|
|
24
|
-
description: "Analyzes codebases and reports findings",
|
|
25
|
-
systemPrompt: "You are a code analysis agent. Given a codebase and a question, thoroughly investigate the relevant files and report your findings. Focus on accuracy — read the actual code rather than guessing.",
|
|
26
|
-
tools: ["read", "bash"],
|
|
27
|
-
output: "analysis.md",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
type: "agent",
|
|
32
|
-
name: "Code Reviewer",
|
|
33
|
-
config: {
|
|
34
|
-
description: "Reviews code for bugs, style, and correctness",
|
|
35
|
-
systemPrompt: "You are a code review agent. Examine the code changes or files provided and identify bugs, style issues, performance concerns, and correctness problems. Be specific — cite line numbers and explain why each issue matters.",
|
|
36
|
-
tools: ["read", "bash"],
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
type: "agent",
|
|
41
|
-
name: "Planner",
|
|
42
|
-
config: {
|
|
43
|
-
description: "Creates implementation plans from requirements",
|
|
44
|
-
systemPrompt: "You are a planning agent. Given a task or requirements, create a detailed implementation plan. Break the work into concrete steps, identify which files need changes, and note any risks or dependencies.",
|
|
45
|
-
tools: ["read", "bash"],
|
|
46
|
-
output: "plan.md",
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
type: "agent",
|
|
51
|
-
name: "Implementer",
|
|
52
|
-
config: {
|
|
53
|
-
description: "Implements code changes from a plan",
|
|
54
|
-
systemPrompt: "You are an implementation agent. Given a plan or task, make the necessary code changes. Write clean, tested code that follows existing patterns. Run tests after making changes.",
|
|
55
|
-
defaultProgress: true,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
{ type: "separator", label: "Chains" },
|
|
59
|
-
{ type: "chain", name: "Blank Chain", description: "Empty chain to configure" },
|
|
60
|
-
];
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import type { ChainConfig, ChainStepConfig } from "../agents/agents.ts";
|
|
4
|
-
import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "../tui/render-helpers.ts";
|
|
5
|
-
import { isParallelStep, type ChainStep } from "../shared/settings.ts";
|
|
6
|
-
|
|
7
|
-
export interface ChainDetailState {
|
|
8
|
-
scrollOffset: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type ChainDetailAction =
|
|
12
|
-
| { type: "back" }
|
|
13
|
-
| { type: "launch" }
|
|
14
|
-
| { type: "edit" };
|
|
15
|
-
|
|
16
|
-
const CHAIN_DETAIL_VIEWPORT_HEIGHT = 12;
|
|
17
|
-
|
|
18
|
-
type DetailChainStep = ChainStepConfig | ChainStep;
|
|
19
|
-
|
|
20
|
-
function buildDependencyMap(steps: DetailChainStep[]): Map<number, number[]> {
|
|
21
|
-
const outputMap = new Map<string, number>();
|
|
22
|
-
const deps = new Map<number, number[]>();
|
|
23
|
-
for (let i = 0; i < steps.length; i++) {
|
|
24
|
-
const step = steps[i]!;
|
|
25
|
-
if (isParallelStep(step as ChainStep)) {
|
|
26
|
-
const reads = step.parallel.flatMap((task) => Array.isArray(task.reads) ? task.reads : []);
|
|
27
|
-
const sources = reads
|
|
28
|
-
.map((file) => outputMap.get(file))
|
|
29
|
-
.filter((idx): idx is number => idx !== undefined);
|
|
30
|
-
if (sources.length > 0) deps.set(i, [...new Set(sources)]);
|
|
31
|
-
for (const task of step.parallel) {
|
|
32
|
-
if (typeof task.output === "string" && task.output.length > 0) outputMap.set(task.output, i);
|
|
33
|
-
}
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (typeof step.output === "string" && step.output.length > 0) outputMap.set(step.output, i);
|
|
37
|
-
if (Array.isArray(step.reads) && step.reads.length > 0) {
|
|
38
|
-
const sources = step.reads
|
|
39
|
-
.map((file) => outputMap.get(file))
|
|
40
|
-
.filter((idx): idx is number => idx !== undefined);
|
|
41
|
-
if (sources.length > 0) deps.set(i, sources);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return deps;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function buildChainDetailLines(chain: ChainConfig, width: number): string[] {
|
|
48
|
-
const contentWidth = width - 3;
|
|
49
|
-
const lines: string[] = [];
|
|
50
|
-
const steps = chain.steps as DetailChainStep[];
|
|
51
|
-
const dependencyMap = buildDependencyMap(steps);
|
|
52
|
-
lines.push(truncateToWidth(chain.description, contentWidth));
|
|
53
|
-
if (chain.packageName) {
|
|
54
|
-
lines.push(truncateToWidth(`Local name: ${chain.localName ?? chain.name}`, contentWidth));
|
|
55
|
-
lines.push(truncateToWidth(`Package: ${chain.packageName}`, contentWidth));
|
|
56
|
-
}
|
|
57
|
-
lines.push("");
|
|
58
|
-
lines.push(truncateToWidth(`File: ${formatPath(chain.filePath)}`, contentWidth));
|
|
59
|
-
lines.push("");
|
|
60
|
-
lines.push(truncateToWidth("── Flow ──", contentWidth));
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < steps.length; i++) {
|
|
63
|
-
const step = steps[i]!;
|
|
64
|
-
const sources = dependencyMap.get(i);
|
|
65
|
-
const fromText = sources && sources.length > 0 ? ` (from ${sources.map((s) => s + 1).join(", ")})` : "";
|
|
66
|
-
if (isParallelStep(step as ChainStep)) {
|
|
67
|
-
lines.push(truncateToWidth(` ${i + 1} Parallel: ${step.parallel.map((task) => task.agent).join(" + ")}`, contentWidth));
|
|
68
|
-
if (step.concurrency !== undefined) lines.push(truncateToWidth(` concurrency: ${step.concurrency}`, contentWidth));
|
|
69
|
-
if (step.failFast !== undefined) lines.push(truncateToWidth(` fail fast: ${step.failFast ? "on" : "off"}`, contentWidth));
|
|
70
|
-
if (step.worktree !== undefined) lines.push(truncateToWidth(` worktree: ${step.worktree ? "on" : "off"}`, contentWidth));
|
|
71
|
-
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
72
|
-
const task = step.parallel[taskIndex]!;
|
|
73
|
-
lines.push(truncateToWidth(` ${taskIndex + 1}. ${task.agent}`, contentWidth));
|
|
74
|
-
const taskPreview = (task.task ?? "").split("\n")[0] ?? "";
|
|
75
|
-
if (taskPreview) lines.push(truncateToWidth(` task: ${taskPreview}`, contentWidth));
|
|
76
|
-
if (Array.isArray(task.reads) && task.reads.length > 0) lines.push(truncateToWidth(` ← reads: ${task.reads.join(", ")}${fromText}`, contentWidth));
|
|
77
|
-
else if (task.reads === false) lines.push(truncateToWidth(" ← reads: (disabled)", contentWidth));
|
|
78
|
-
if (typeof task.output === "string" && task.output.length > 0) lines.push(truncateToWidth(` → output: ${task.output}`, contentWidth));
|
|
79
|
-
else if (task.output === false) lines.push(truncateToWidth(" → output: (disabled)", contentWidth));
|
|
80
|
-
if (task.model) lines.push(truncateToWidth(` model: ${task.model}`, contentWidth));
|
|
81
|
-
if (task.skill !== undefined) {
|
|
82
|
-
const skillsText =
|
|
83
|
-
task.skill === false
|
|
84
|
-
? "(disabled)"
|
|
85
|
-
: Array.isArray(task.skill)
|
|
86
|
-
? (task.skill.length > 0 ? task.skill.join(", ") : "(none)")
|
|
87
|
-
: task.skill;
|
|
88
|
-
lines.push(truncateToWidth(` skills: ${skillsText}`, contentWidth));
|
|
89
|
-
}
|
|
90
|
-
if (task.progress !== undefined) lines.push(truncateToWidth(` progress: ${task.progress ? "on" : "off"}`, contentWidth));
|
|
91
|
-
}
|
|
92
|
-
lines.push("");
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
lines.push(truncateToWidth(` ${i + 1} ${step.agent}`, contentWidth));
|
|
96
|
-
const taskPreview = step.task.split("\n")[0] ?? "";
|
|
97
|
-
lines.push(truncateToWidth(` task: ${taskPreview || "(none)"}`, contentWidth));
|
|
98
|
-
if (Array.isArray(step.reads) && step.reads.length > 0) {
|
|
99
|
-
lines.push(truncateToWidth(` ← reads: ${step.reads.join(", ")}${fromText}`, contentWidth));
|
|
100
|
-
} else if (step.reads === false) {
|
|
101
|
-
lines.push(truncateToWidth(" ← reads: (disabled)", contentWidth));
|
|
102
|
-
}
|
|
103
|
-
if (typeof step.output === "string" && step.output.length > 0) {
|
|
104
|
-
lines.push(truncateToWidth(` → output: ${step.output}`, contentWidth));
|
|
105
|
-
} else if (step.output === false) {
|
|
106
|
-
lines.push(truncateToWidth(" → output: (disabled)", contentWidth));
|
|
107
|
-
}
|
|
108
|
-
if (step.model) lines.push(truncateToWidth(` model: ${step.model}`, contentWidth));
|
|
109
|
-
if (step.skills !== undefined) {
|
|
110
|
-
const skillsText =
|
|
111
|
-
step.skills === false
|
|
112
|
-
? "(disabled)"
|
|
113
|
-
: step.skills.length > 0
|
|
114
|
-
? step.skills.join(", ")
|
|
115
|
-
: "(none)";
|
|
116
|
-
lines.push(truncateToWidth(` skills: ${skillsText}`, contentWidth));
|
|
117
|
-
}
|
|
118
|
-
if (step.progress !== undefined) {
|
|
119
|
-
lines.push(truncateToWidth(` progress: ${step.progress ? "on" : "off"}`, contentWidth));
|
|
120
|
-
}
|
|
121
|
-
lines.push("");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (chain.steps.length === 0) {
|
|
125
|
-
lines.push(truncateToWidth("(no steps)", contentWidth));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return lines;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function handleChainDetailInput(state: ChainDetailState, data: string): ChainDetailAction | undefined {
|
|
132
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { type: "back" };
|
|
133
|
-
if (data === "l") return { type: "launch" };
|
|
134
|
-
if (data === "e") return { type: "edit" };
|
|
135
|
-
if (matchesKey(data, "up")) { state.scrollOffset--; return; }
|
|
136
|
-
if (matchesKey(data, "down")) { state.scrollOffset++; return; }
|
|
137
|
-
if (matchesKey(data, "pageup") || matchesKey(data, "shift+up")) { state.scrollOffset -= CHAIN_DETAIL_VIEWPORT_HEIGHT; return; }
|
|
138
|
-
if (matchesKey(data, "pagedown") || matchesKey(data, "shift+down")) { state.scrollOffset += CHAIN_DETAIL_VIEWPORT_HEIGHT; return; }
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function renderChainDetail(
|
|
143
|
-
state: ChainDetailState,
|
|
144
|
-
chain: ChainConfig,
|
|
145
|
-
width: number,
|
|
146
|
-
theme: Theme,
|
|
147
|
-
): string[] {
|
|
148
|
-
const lines: string[] = [];
|
|
149
|
-
const scopeBadge = chain.source === "user" ? "[user]" : "[proj]";
|
|
150
|
-
lines.push(renderHeader(` ${chain.name} [chain] ${scopeBadge} `, width, theme));
|
|
151
|
-
lines.push(row("", width, theme));
|
|
152
|
-
|
|
153
|
-
const contentLines = buildChainDetailLines(chain, width);
|
|
154
|
-
const maxOffset = Math.max(0, contentLines.length - CHAIN_DETAIL_VIEWPORT_HEIGHT);
|
|
155
|
-
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset, maxOffset));
|
|
156
|
-
const visible = contentLines.slice(state.scrollOffset, state.scrollOffset + CHAIN_DETAIL_VIEWPORT_HEIGHT);
|
|
157
|
-
for (const line of visible) lines.push(row(` ${line}`, width, theme));
|
|
158
|
-
for (let i = visible.length; i < CHAIN_DETAIL_VIEWPORT_HEIGHT; i++) lines.push(row("", width, theme));
|
|
159
|
-
|
|
160
|
-
const scrollInfo = formatScrollInfo(state.scrollOffset, Math.max(0, contentLines.length - (state.scrollOffset + CHAIN_DETAIL_VIEWPORT_HEIGHT)));
|
|
161
|
-
lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
|
|
162
|
-
lines.push(renderFooter(" [l]aunch [e]dit [↑↓] scroll [esc] back ", width, theme));
|
|
163
|
-
return lines;
|
|
164
|
-
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import type { AgentConfig } from "../agents/agents.ts";
|
|
4
|
-
import { formatDuration } from "../shared/formatters.ts";
|
|
5
|
-
import type { RunEntry } from "../runs/shared/run-history.ts";
|
|
6
|
-
import { buildSkillInjection, resolveSkills } from "../agents/skills.ts";
|
|
7
|
-
import { ensureCursorVisible, getCursorDisplayPos, renderEditor, wrapText } from "../tui/text-editor.ts";
|
|
8
|
-
import type { TextEditorState } from "../tui/text-editor.ts";
|
|
9
|
-
import { pad, row, renderHeader, renderFooter, formatPath, formatScrollInfo } from "../tui/render-helpers.ts";
|
|
10
|
-
|
|
11
|
-
export interface DetailState {
|
|
12
|
-
resolved: boolean;
|
|
13
|
-
scrollOffset: number;
|
|
14
|
-
recentRuns?: RunEntry[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type DetailAction =
|
|
18
|
-
| { type: "back" }
|
|
19
|
-
| { type: "edit" }
|
|
20
|
-
| { type: "launch" };
|
|
21
|
-
|
|
22
|
-
const DETAIL_VIEWPORT_HEIGHT = 12;
|
|
23
|
-
|
|
24
|
-
function renderFieldLine(
|
|
25
|
-
label: string,
|
|
26
|
-
value: string,
|
|
27
|
-
width: number,
|
|
28
|
-
theme: Theme,
|
|
29
|
-
): string {
|
|
30
|
-
const labelWidth = 12;
|
|
31
|
-
const labelText = theme.fg("dim", pad(label, labelWidth));
|
|
32
|
-
const available = Math.max(0, width - labelWidth);
|
|
33
|
-
return `${labelText}${truncateToWidth(value, available)}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function formatRelativeTime(ts: number): string {
|
|
37
|
-
const diff = Math.max(0, Math.floor(Date.now() / 1000) - ts);
|
|
38
|
-
if (diff < 60) return `${diff}s ago`;
|
|
39
|
-
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
40
|
-
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
41
|
-
return `${Math.floor(diff / 86400)}d ago`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function buildDetailLines(
|
|
45
|
-
agent: AgentConfig,
|
|
46
|
-
resolved: boolean,
|
|
47
|
-
recentRuns: RunEntry[] | undefined,
|
|
48
|
-
cwd: string,
|
|
49
|
-
width: number,
|
|
50
|
-
theme: Theme,
|
|
51
|
-
): string[] {
|
|
52
|
-
const contentWidth = width - 3;
|
|
53
|
-
const lines: string[] = [];
|
|
54
|
-
|
|
55
|
-
const tools = agent.tools && agent.tools.length > 0 ? agent.tools.join(", ") : "(none)";
|
|
56
|
-
const mcp = agent.mcpDirectTools && agent.mcpDirectTools.length > 0 ? agent.mcpDirectTools.join(", ") : "(none)";
|
|
57
|
-
const skillsList = agent.skills && agent.skills.length > 0 ? agent.skills.join(", ") : "(none)";
|
|
58
|
-
const output = agent.output ?? "(none)";
|
|
59
|
-
const reads = agent.defaultReads && agent.defaultReads.length > 0 ? agent.defaultReads.join(", ") : "(none)";
|
|
60
|
-
const progress = agent.defaultProgress ? "on" : "off";
|
|
61
|
-
const defaultContext = agent.defaultContext ?? "auto";
|
|
62
|
-
const maxSubagentDepth = agent.maxSubagentDepth !== undefined ? String(agent.maxSubagentDepth) : "(default)";
|
|
63
|
-
|
|
64
|
-
if (agent.packageName) {
|
|
65
|
-
lines.push(renderFieldLine("Local name:", agent.localName ?? agent.name, contentWidth, theme));
|
|
66
|
-
lines.push(renderFieldLine("Package:", agent.packageName, contentWidth, theme));
|
|
67
|
-
}
|
|
68
|
-
lines.push(renderFieldLine("Model:", agent.model ?? "default", contentWidth, theme));
|
|
69
|
-
lines.push(renderFieldLine("Prompt mode:", agent.systemPromptMode, contentWidth, theme));
|
|
70
|
-
lines.push(renderFieldLine("Project ctx:", agent.inheritProjectContext ? "on" : "off", contentWidth, theme));
|
|
71
|
-
lines.push(renderFieldLine("Skills ctx:", agent.inheritSkills ? "on" : "off", contentWidth, theme));
|
|
72
|
-
lines.push(renderFieldLine("Run context:", defaultContext, contentWidth, theme));
|
|
73
|
-
if (agent.source === "builtin") {
|
|
74
|
-
lines.push(renderFieldLine("Disabled:", agent.disabled ? "on" : "off", contentWidth, theme));
|
|
75
|
-
}
|
|
76
|
-
if (agent.override) {
|
|
77
|
-
const overrideLabel = `${agent.override.scope} · ${formatPath(agent.override.path)}`;
|
|
78
|
-
lines.push(renderFieldLine("Override:", overrideLabel, contentWidth, theme));
|
|
79
|
-
}
|
|
80
|
-
lines.push(renderFieldLine("Thinking:", agent.thinking ?? "off", contentWidth, theme));
|
|
81
|
-
lines.push(renderFieldLine("Tools:", tools, contentWidth, theme));
|
|
82
|
-
lines.push(renderFieldLine("MCP:", mcp, contentWidth, theme));
|
|
83
|
-
lines.push(renderFieldLine("Skills:", skillsList, contentWidth, theme));
|
|
84
|
-
const extensionsList = agent.extensions !== undefined
|
|
85
|
-
? (agent.extensions.length > 0 ? agent.extensions.join(", ") : "(none)")
|
|
86
|
-
: "(all)";
|
|
87
|
-
lines.push(renderFieldLine("Extensions:", extensionsList, contentWidth, theme));
|
|
88
|
-
lines.push(renderFieldLine("Output:", output, contentWidth, theme));
|
|
89
|
-
lines.push(renderFieldLine("Reads:", reads, contentWidth, theme));
|
|
90
|
-
lines.push(renderFieldLine("Progress:", progress, contentWidth, theme));
|
|
91
|
-
lines.push(renderFieldLine("Max depth:", maxSubagentDepth, contentWidth, theme));
|
|
92
|
-
|
|
93
|
-
if (agent.extraFields) {
|
|
94
|
-
for (const [key, value] of Object.entries(agent.extraFields)) {
|
|
95
|
-
lines.push(truncateToWidth(`${key}: ${value}`, contentWidth));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
lines.push("");
|
|
100
|
-
const sectionTitle = `── System Prompt (${resolved ? "resolved" : "raw"}) ──`;
|
|
101
|
-
lines.push(truncateToWidth(sectionTitle, contentWidth));
|
|
102
|
-
|
|
103
|
-
let prompt = agent.systemPrompt ?? "";
|
|
104
|
-
if (resolved) {
|
|
105
|
-
const { resolved: resolvedSkills } = resolveSkills(agent.skills ?? [], cwd);
|
|
106
|
-
const injection = buildSkillInjection(resolvedSkills);
|
|
107
|
-
if (injection) prompt = `${prompt}\n\n${injection}`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const wrapped = wrapText(prompt, contentWidth);
|
|
111
|
-
lines.push(...wrapped.lines);
|
|
112
|
-
lines.push("");
|
|
113
|
-
lines.push(truncateToWidth("── Recent Runs ──", contentWidth));
|
|
114
|
-
if (!recentRuns || recentRuns.length === 0) {
|
|
115
|
-
lines.push(truncateToWidth(" (none)", contentWidth));
|
|
116
|
-
return lines;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const run of recentRuns) {
|
|
120
|
-
const when = pad(formatRelativeTime(run.ts), 8);
|
|
121
|
-
const status = run.status;
|
|
122
|
-
const task = truncateToWidth(`"${run.task}"`, 34);
|
|
123
|
-
const tail = run.status === "ok" ? formatDuration(run.duration) : `exit ${run.exit ?? 1}`;
|
|
124
|
-
lines.push(truncateToWidth(` ${when} ${status} ${task} ${tail}`, contentWidth));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return lines;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function handleDetailInput(state: DetailState, data: string): DetailAction | undefined {
|
|
131
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { type: "back" };
|
|
132
|
-
if (data === "e") return { type: "edit" };
|
|
133
|
-
if (data === "l") return { type: "launch" };
|
|
134
|
-
if (data === "v") { state.resolved = !state.resolved; state.scrollOffset = 0; return; }
|
|
135
|
-
if (matchesKey(data, "up")) { state.scrollOffset--; return; }
|
|
136
|
-
if (matchesKey(data, "down")) { state.scrollOffset++; return; }
|
|
137
|
-
if (matchesKey(data, "pageup") || matchesKey(data, "shift+up")) { state.scrollOffset -= DETAIL_VIEWPORT_HEIGHT; return; }
|
|
138
|
-
if (matchesKey(data, "pagedown") || matchesKey(data, "shift+down")) { state.scrollOffset += DETAIL_VIEWPORT_HEIGHT; return; }
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function renderDetail(
|
|
143
|
-
state: DetailState,
|
|
144
|
-
agent: AgentConfig,
|
|
145
|
-
cwd: string,
|
|
146
|
-
width: number,
|
|
147
|
-
theme: Theme,
|
|
148
|
-
): string[] {
|
|
149
|
-
const lines: string[] = [];
|
|
150
|
-
const scopeBadge = agent.source === "builtin"
|
|
151
|
-
? (agent.disabled
|
|
152
|
-
? (agent.override ? `[builtin off+${agent.override.scope}]` : "[builtin off]")
|
|
153
|
-
: (agent.override ? `[builtin+${agent.override.scope}]` : "[builtin]"))
|
|
154
|
-
: agent.source === "project"
|
|
155
|
-
? "[proj]"
|
|
156
|
-
: "[user]";
|
|
157
|
-
const headerText = ` ${agent.name} ${scopeBadge} ${formatPath(agent.filePath)} `;
|
|
158
|
-
lines.push(renderHeader(headerText, width, theme));
|
|
159
|
-
lines.push(row("", width, theme));
|
|
160
|
-
|
|
161
|
-
const contentLines = buildDetailLines(agent, state.resolved, state.recentRuns, cwd, width, theme);
|
|
162
|
-
const maxOffset = Math.max(0, contentLines.length - DETAIL_VIEWPORT_HEIGHT);
|
|
163
|
-
state.scrollOffset = Math.max(0, Math.min(state.scrollOffset, maxOffset));
|
|
164
|
-
|
|
165
|
-
const visible = contentLines.slice(state.scrollOffset, state.scrollOffset + DETAIL_VIEWPORT_HEIGHT);
|
|
166
|
-
for (const line of visible) {
|
|
167
|
-
lines.push(row(` ${line}`, width, theme));
|
|
168
|
-
}
|
|
169
|
-
for (let i = visible.length; i < DETAIL_VIEWPORT_HEIGHT; i++) {
|
|
170
|
-
lines.push(row("", width, theme));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const scrollInfo = formatScrollInfo(state.scrollOffset, Math.max(0, contentLines.length - (state.scrollOffset + DETAIL_VIEWPORT_HEIGHT)));
|
|
174
|
-
lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
|
|
175
|
-
|
|
176
|
-
const footer = agent.source === "builtin"
|
|
177
|
-
? agent.override
|
|
178
|
-
? (agent.disabled
|
|
179
|
-
? " [e]dit override [v] raw/resolved [↑↓] scroll [esc] back "
|
|
180
|
-
: " [l]aunch [e]dit override [v] raw/resolved [↑↓] scroll [esc] back ")
|
|
181
|
-
: (agent.disabled
|
|
182
|
-
? " [e]create override [v] raw/resolved [↑↓] scroll [esc] back "
|
|
183
|
-
: " [l]aunch [e]create override [v] raw/resolved [↑↓] scroll [esc] back ")
|
|
184
|
-
: " [l]aunch [e]dit [v] raw/resolved [↑↓] scroll [esc] back ";
|
|
185
|
-
lines.push(renderFooter(footer, width, theme));
|
|
186
|
-
return lines;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export interface LaunchToggleState {
|
|
190
|
-
fork: boolean;
|
|
191
|
-
background: boolean;
|
|
192
|
-
worktree?: boolean;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function renderTaskInput(
|
|
196
|
-
title: string,
|
|
197
|
-
editor: TextEditorState,
|
|
198
|
-
skipClarify: boolean,
|
|
199
|
-
width: number,
|
|
200
|
-
theme: Theme,
|
|
201
|
-
launchToggles?: LaunchToggleState,
|
|
202
|
-
): string[] {
|
|
203
|
-
const lines: string[] = [];
|
|
204
|
-
lines.push(renderHeader(` ${title} `, width, theme));
|
|
205
|
-
lines.push(row("", width, theme));
|
|
206
|
-
lines.push(row(` ${theme.fg("dim", "Task:")}`, width, theme));
|
|
207
|
-
|
|
208
|
-
const innerW = width - 2;
|
|
209
|
-
const boxInnerWidth = Math.max(10, innerW - 4);
|
|
210
|
-
const top = `┌${"─".repeat(boxInnerWidth)}┐`;
|
|
211
|
-
const bottom = `└${"─".repeat(boxInnerWidth)}┘`;
|
|
212
|
-
|
|
213
|
-
lines.push(row(` ${top}`, width, theme));
|
|
214
|
-
const editorState = { ...editor };
|
|
215
|
-
const { starts } = wrapText(editorState.buffer, boxInnerWidth);
|
|
216
|
-
const cursorPos = getCursorDisplayPos(editorState.cursor, starts);
|
|
217
|
-
editorState.viewportOffset = ensureCursorVisible(cursorPos.line, 2, editorState.viewportOffset);
|
|
218
|
-
const editorLines = renderEditor(editorState, boxInnerWidth, 2);
|
|
219
|
-
for (const line of editorLines) {
|
|
220
|
-
lines.push(row(` │${pad(line, boxInnerWidth)}│`, width, theme));
|
|
221
|
-
}
|
|
222
|
-
lines.push(row(` ${bottom}`, width, theme));
|
|
223
|
-
|
|
224
|
-
lines.push(row("", width, theme));
|
|
225
|
-
const quickLabel = skipClarify ? "on" : "off";
|
|
226
|
-
const footerParts = ["[enter] run", `[tab] quick:${quickLabel}`];
|
|
227
|
-
if (launchToggles) {
|
|
228
|
-
footerParts.push(`[ctrl+f] fork:${launchToggles.fork ? "on" : "off"}`);
|
|
229
|
-
footerParts.push(`[ctrl+b] bg:${launchToggles.background ? "on" : "off"}`);
|
|
230
|
-
if (launchToggles.worktree !== undefined) footerParts.push(`[ctrl+w] worktree:${launchToggles.worktree ? "on" : "off"}`);
|
|
231
|
-
}
|
|
232
|
-
footerParts.push("[esc]");
|
|
233
|
-
lines.push(renderFooter(` ${footerParts.join(" ")} `, width, theme));
|
|
234
|
-
return lines;
|
|
235
|
-
}
|