Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ cwd model thinking context cache cost throughput git PR
- 支持 `powerline`、`powerline-mono`、`compact`,也支持自定义多行布局;
- 终端变窄时按优先级隐藏次要指标,不机械截断尾部;
- Subagent 与 Workflow 活动时自动出现,空闲时不占空间;
- Bash、Write/Edit 与 Subagent 结果可独立选择 `full` 或 `compact`;
- Bash、Write/Edit 与 Subagent 结果可独立选择 `full` 或 `compact`;Subagent 的 compact 模式只显示状态摘要,不暴露原始子 Agent 正文;
- 折叠内容用 Pi 的 `app.tools.expand` 快捷键临时展开,默认 `Ctrl+O`;
- Git 状态本地刷新;只有显式运行 `/pr` 才查询 GitHub PR。

Expand Down
2 changes: 1 addition & 1 deletion SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Use the single canonical package-owned command. `/my-pi-setup` remains a compati
/openpi-setup 清除 explorer 的模型,让它继承父模型
```

Capability discovery defaults to `explicit`, preserving the zero-resident OpenPI tool surface until the user asks for a capability. `adaptive` is an explicit opt-in that keeps only `openpi_load_tools` visible and allows the model to load a useful group on its own; because this can start Subagents, Workflows, or background processes, normal permission and configured concurrency/call limits still apply. Changing the setting updates the current Session immediately, while already loaded groups remain stable for that Session. Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. `Right` accepts it into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; users who do not usually inspect implementation detail can select compact previews. Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand` (`Ctrl+O` by default). An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.
Capability discovery defaults to `explicit`, preserving the zero-resident OpenPI tool surface until the user asks for a capability. `adaptive` is an explicit opt-in that keeps only `openpi_load_tools` visible and allows the model to load a useful group on its own; because this can start Subagents, Workflows, or background processes, normal permission and configured concurrency/call limits still apply. Changing the setting updates the current Session immediately, while already loaded groups remain stable for that Session. Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. `Right` accepts it into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; compact mode shows only bounded status rows and keeps raw child reports behind `app.tools.expand` (`Ctrl+O` by default). Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand`. An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.

## Session Goal and Tasks

Expand Down
4 changes: 2 additions & 2 deletions extensions/setup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function buildInteractiveSetupPrompt(options: {
"- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text); the default is plain with cwd/git/pr on the left and model/context/cost on the right. Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Nerd Font only affects powerline separator glyphs; text stays readable without it. Changes apply immediately in the active TUI session.",
"- Operational activity for Subagents, Workflows, and background terminals is core status and always remains visible whenever the custom footer is enabled.",
"- Post-edit command: one optional shell command (maximum 500 characters) run in the background after a turn with successful Write/Edit operations (e.g. `npm run format`). Off by default, interactive TUI sessions only, failures surface as a notification. This is a single command, not an event-hook system.",
"- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full (always expanded) or compact (Claude Code-style folded preview with a hidden-line count). Compact output can still be temporarily expanded with the configured app.tools.expand key (Ctrl+O by default). Bash and Write/Edit default to compact. Recommend compact for users who do not usually inspect implementation details.",
"- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full or compact. Compact Subagent results show only bounded status rows and keep raw child reports behind app.tools.expand; compact Bash and Write/Edit operations use folded previews. Ctrl+O expands compact output by default. Bash and Write/Edit default to compact. Recommend compact for users who do not usually inspect implementation details.",
"- Agent role models: built-in explorer, implementer, reviewer, and advisor roles are shared by subagent_spawn and workflow agent_type, and inherit the parent model by default. Assign only an available registry model to an individual role when needed; clearing that role returns it to inheritance. Custom agent-type files still override a built-in role's complete definition.",
"- Intercom: optional cross-session messaging is installed only after a native setup confirmation. It stays parent-only; Direct/Workflow children and Replay cannot use it. The status above is informational for this model-guided step—do not install packages or edit its config yourself.",
"",
Expand Down Expand Up @@ -395,7 +395,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
subagent_result_display: Type.Optional(
StringEnum(DETAIL_DISPLAYS, {
description:
"How completed Subagent results render by default: full preserves complete output; compact shows a bounded preview that can be expanded with app.tools.expand. Omit to preserve the current value.",
"How completed Subagent results render by default: full shows complete output; compact shows only bounded status rows while app.tools.expand reveals the full child report. Omit to preserve the current value.",
}),
),
bash_tool_display: Type.Optional(
Expand Down
2 changes: 1 addition & 1 deletion extensions/shared/setup-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ export function formatSetupConfig(
suggestions,
`Workflows: ${config.workflows.concurrency} concurrent agents · ${config.workflows.maxAgentCalls} total calls`,
`UI: large header ${config.ui.showHeader ? "on" : "off"} · custom footer ${footer}`,
`Subagent results: ${config.ui.subagentResultDisplay === "full" ? "full by default" : "compact preview (expand for full output)"}`,
`Subagent results: ${config.ui.subagentResultDisplay === "full" ? "full by default" : "compact status summary (Ctrl+O expands full output)"}`,
`Bash operations: ${config.ui.bashToolDisplay === "full" ? "expanded by default" : "folded preview (Ctrl+O expands all)"}`,
`Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "folded preview (Ctrl+O expands all)"}`,
`Post-edit command: ${config.postEdit.command ? config.postEdit.command : "off"}`,
Expand Down
50 changes: 28 additions & 22 deletions extensions/subagents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ import {
} from "./navigation.ts";
import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
import {
buildWaitResultPreview,
renderWaitResult,
type WaitResultDetails,
} from "./src/ui/wait-result.ts";
Expand Down Expand Up @@ -261,6 +262,21 @@ function renderSubagentResult(
expanded: boolean,
theme: SubagentResultTheme,
) {
if (!expanded && loadSetupConfig().ui.subagentResultDisplay === "compact") {
const results = details.results?.length
? details.results
: details.id
? [
{
id: details.id,
title: details.title,
status: details.status,
},
]
: [];
return new Text(buildWaitResultPreview(content, { results }, theme), 0, 0);
}

const failed = details.status === "error";
const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
const header =
Expand All @@ -274,28 +290,18 @@ function renderSubagentResult(
// Remove only the summary line. The following Error line (when present)
// is part of the actual result and must remain visible.
const body = content.split("\n").slice(1).join("\n").trim();
if (expanded || loadSetupConfig().ui.subagentResultDisplay === "full") {
const md = new Markdown(body, 0, 0, getMarkdownTheme());
const container = new Text(header, 0, 0);
return {
render: (width: number) => [
...container.render(width),
...md.render(width),
],
invalidate: () => {
container.invalidate();
md.invalidate();
},
};
}

const bodyLines = body.split("\n");
let text = header;
for (const line of bodyLines.slice(0, 8))
text += `\n${theme.fg("toolOutput", line)}`;
if (bodyLines.length > 8)
text += `\n${theme.fg("dim", `... (${keyHint("app.tools.expand", "to expand")})`)}`;
return new Text(text, 0, 0);
const md = new Markdown(body, 0, 0, getMarkdownTheme());
const container = new Text(header, 0, 0);
return {
render: (width: number) => [
...container.render(width),
...md.render(width),
],
invalidate: () => {
container.invalidate();
md.invalidate();
},
};
}

export default function (pi: ExtensionAPI) {
Expand Down
87 changes: 87 additions & 0 deletions extensions/subagents/result-rendering.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import {
initTheme,
type EntryRenderer,
type ExtensionAPI,
} from "@earendil-works/pi-coding-agent";

initTheme("dark", false);

test("compact automatic results keep the report behind expansion", async () => {
const agentDir = await mkdtemp(path.join(tmpdir(), "openpi-result-render-"));
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
process.env.PI_CODING_AGENT_DIR = agentDir;

try {
await writeFile(
path.join(agentDir, "my-pi-setup.json"),
JSON.stringify({ ui: { subagentResultDisplay: "compact" } }),
);
const { default: subagents } = await import("./index.ts");
const renderers = new Map<string, EntryRenderer>();
const pi = {
on() {},
events: { on() {} },
registerTool() {},
getActiveTools: () => [],
setActiveTools() {},
registerMessageRenderer() {},
registerEntryRenderer(customType: string, renderer: EntryRenderer) {
renderers.set(customType, renderer);
},
registerCommand() {},
} as unknown as ExtensionAPI;
subagents(pi);

const renderer = renderers.get("subagent-result");
assert.ok(renderer);
const theme = {
fg: (_color: string, text: string) => text,
bg: (_color: string, text: string) => text,
bold: (text: string) => text,
italic: (text: string) => text,
underline: (text: string) => text,
strikethrough: (text: string) => text,
inverse: (text: string) => text,
} as unknown as Parameters<EntryRenderer>[2];
const entry = {
type: "custom" as const,
id: "entry-1",
parentId: null,
timestamp: new Date().toISOString(),
customType: "subagent-result",
data: {
content:
'Subagent sa-3 "investigate plan mode" finished.\n\nPlan Mode investigation report',
details: {
id: "sa-3",
title: "investigate plan mode",
status: "done" as const,
},
},
};

const compact = renderer(entry, { expanded: false }, theme);
assert.ok(compact);
const compactText = compact.render(120).join("\n");
assert.match(compactText, /1 subagent settled/);
assert.match(compactText, /sa-3 · investigate plan mode · done/);
assert.match(compactText, /Results passed to main agent/);
assert.doesNotMatch(compactText, /Plan Mode investigation report/);

const expanded = renderer(entry, { expanded: true }, theme);
assert.ok(expanded);
assert.match(
expanded.render(120).join("\n"),
/Plan Mode investigation report/,
);
} finally {
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
await rm(agentDir, { recursive: true, force: true });
}
});
25 changes: 6 additions & 19 deletions extensions/subagents/src/ui/wait-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@ import {
import { Markdown, Text } from "@earendil-works/pi-tui";
import { sanitizeText } from "./transcript.ts";

const COLLAPSED_MAX_LINES = 12;
const MAX_STATUS_ROWS = 4;

export interface WaitResultItem {
id: string;
title?: string;
status?: string;
readonly id: string;
readonly title?: string;
readonly status?: string;
}

export interface WaitResultDetails {
results?: WaitResultItem[];
readonly results?: readonly WaitResultItem[];
}

export function buildWaitResultPreview(
Expand Down Expand Up @@ -46,23 +45,11 @@ export function buildWaitResultPreview(
lines.push(theme.fg("dim", ` … ${results.length - MAX_STATUS_ROWS} more`));
}

const cleanLines = sanitizeText(content)
.split("\n")
.map((line) => line.trimEnd())
.filter((line) => line.trim() && line.trim() !== "---");
const leadingHeader = cleanLines[0]?.startsWith("## ") ? 1 : 0;
const body = cleanLines.slice(leadingHeader);
const available = Math.max(1, COLLAPSED_MAX_LINES - lines.length - 1);
for (const line of body.slice(0, available)) {
lines.push(theme.fg("toolOutput", line));
}

const hidden = Math.max(0, body.length - available);
if (hidden > 0) {
if (content.trim()) {
lines.push(
theme.fg(
"dim",
`… ${hidden} more line${hidden === 1 ? "" : "s"} · ${keyHint("app.tools.expand", "to expand")}`,
`Results passed to main agent · ${keyHint("app.tools.expand", "to expand")}`,
),
);
}
Expand Down
7 changes: 4 additions & 3 deletions extensions/subagents/wait-result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const theme = {
bold: (text: string) => text,
} as Theme;

test("wait result preview has a global line budget and reports hidden lines", () => {
test("wait result preview shows status only and keeps full output behind expand", () => {
const content = [
'## sa-1 "review" finished',
"",
Expand All @@ -28,12 +28,13 @@ test("wait result preview has a global line budget and reports hidden lines", ()
);
const lines = preview.split("\n");

assert.ok(lines.length <= 12);
assert.ok(lines.length <= 4);
assert.match(preview, /2 subagents settled · 1 failed/);
assert.match(preview, /sa-1 · review · done/);
assert.match(preview, /sa-2 · tests · error/);
assert.match(preview, /more lines/);
assert.match(preview, /Results passed to main agent/);
assert.match(preview, /expand/);
assert.doesNotMatch(preview, /finding 1/);
assert.doesNotMatch(preview, /finding 30/);
});

Expand Down
Loading