diff --git a/README.md b/README.md index 43e8b775..c63d2a26 100644 --- a/README.md +++ b/README.md @@ -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。 diff --git a/SETUP.md b/SETUP.md index 9045ee43..7ce7ced4 100644 --- a/SETUP.md +++ b/SETUP.md @@ -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/.md` overrides global `~/.pi/agent/agents/.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/.md` overrides global `~/.pi/agent/agents/.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 diff --git a/extensions/setup/index.ts b/extensions/setup/index.ts index e34b47b4..eab0ed8e 100644 --- a/extensions/setup/index.ts +++ b/extensions/setup/index.ts @@ -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.", "", @@ -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( diff --git a/extensions/shared/setup-config.ts b/extensions/shared/setup-config.ts index 5f2ea62e..7d5ed3fb 100644 --- a/extensions/shared/setup-config.ts +++ b/extensions/shared/setup-config.ts @@ -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"}`, diff --git a/extensions/subagents/index.ts b/extensions/subagents/index.ts index a8f79a6d..59d8fd3a 100644 --- a/extensions/subagents/index.ts +++ b/extensions/subagents/index.ts @@ -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"; @@ -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 = @@ -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) { diff --git a/extensions/subagents/result-rendering.test.ts b/extensions/subagents/result-rendering.test.ts new file mode 100644 index 00000000..b13f216f --- /dev/null +++ b/extensions/subagents/result-rendering.test.ts @@ -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(); + 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[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 }); + } +}); diff --git a/extensions/subagents/src/ui/wait-result.ts b/extensions/subagents/src/ui/wait-result.ts index 84398ab7..e04b6f0e 100644 --- a/extensions/subagents/src/ui/wait-result.ts +++ b/extensions/subagents/src/ui/wait-result.ts @@ -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( @@ -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")}`, ), ); } diff --git a/extensions/subagents/wait-result.test.ts b/extensions/subagents/wait-result.test.ts index 862d9d7c..aa44b88b 100644 --- a/extensions/subagents/wait-result.test.ts +++ b/extensions/subagents/wait-result.test.ts @@ -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', "", @@ -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/); });