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/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: string, theme: Theme): string {
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: string, theme: Theme): string {
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
- let flatIndex = 0;
369
- for (let stepIndex = 0; stepIndex < total; stepIndex++) {
370
- const group = groups.find((candidate) => candidate.stepIndex === stepIndex);
371
- if (group) {
372
- const steps = job.steps.slice(group.start, group.start + group.count);
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 = job.steps[flatIndex];
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 formatAgentRunningLabel(count: number): string {
438
- return count === 1 ? "1 agent running" : `${count} agents running`;
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 · /subagents-status")}`,
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 · /subagents-status for output paths"));
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 · /subagents-status for full detail`
707
- : `… ${hiddenCount} lines hidden · Ctrl+O expands · /subagents-status for full detail`;
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 · /subagents-status")}`, width));
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
- }