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 @@ -361,7 +361,7 @@ Footer 使用一套 Codicon 线性图标:`` 模型、`` context、``
- 终端变窄时按优先级隐藏次要指标,不机械截断尾部;
- Subagent 与 Workflow 活动时自动出现,空闲时不占空间;
- Bash、Write/Edit 与 Subagent 结果可独立选择 `full` 或 `compact`;普通 `read`、`grep`、`find`、`ls` 以及 compact Bash/Write/Edit 默认显示一行语义活动摘要,包含目标、状态与关键规模;Nerd Font 可为读取、终端、编辑、搜索和目录动作显示 Codex 风格线框图标,未安装时动词与全部信息仍保持可读;
- 折叠内容用 Pi 的 `app.tools.expand` 快捷键临时展开(默认 `Ctrl+O`),展开后直接恢复 Pi 原生参数、输出、错误、diff、耗时与 full-output 证据;
- 折叠内容用 Pi 的 `app.tools.expand` 快捷键临时展开(默认 `Ctrl+O`),展开后直接恢复 Pi 原生参数、输出、错误、diff、耗时与 full-output 证据;进入 Direct Subagent 或 Workflow child 详情页时会继承父会话的当前展开状态,详情页内切换只影响该页,不改变父会话;
- Git 状态本地刷新;只有显式运行 `/pr` 才查询 GitHub PR。

`fd` 与 `rg` 是结构化模型工具,不拼接 Shell。它们默认遵守 `.gitignore`,支持 Glob、类型、Smart Case、固定字符串与上下文。`git_show`、`git_diff`、`git_log` 以结构化参数提供只读提交、差异和历史检查,并禁用仓库配置的 external diff/textconv。两类工具的输出均限制为 50 KiB / 2000 行,完整截断内容最多私有保存 10 MiB,并在 Session Shutdown 时清理。
Expand Down
73 changes: 73 additions & 0 deletions docs/design/CHILD_TOOL_ACTIVITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Child Tool Activity Design

- Status: validated
- Created: 2026-08-29
- Verified: 2026-08-30
- Source boundary: OpenPI implementation and tests in the pull request that closes issue #93
- Related issue: https://github.com/openpi-dev/openpi/issues/93
- Related pull request: https://github.com/openpi-dev/openpi/pull/292
- Supersedes: none

## Goal

Issue #93 requires Direct Subagent and Workflow child transcript pages to use
the same compact, one-line Pi activity projection for `read`, `grep`, `find`,
`ls`, `bash`, `write`, and `edit`. The default stays compact; the configured
Pi expansion binding reveals Pi's native call and result evidence.

## Scope and Boundaries

This changes only the operator-facing child-session page. It does not change
child tool definitions, tool schemas, execution, permissions, model-visible
messages, transcript artifacts, replay data, or provider requests.

The existing `AgentToolRenderLedger` remains ephemeral. A live Direct or
Workflow child has the original tool definition and result data needed by
Pi's `ToolExecutionComponent`. A persisted Workflow transcript deliberately
has only its bounded text projection, so reopening it without an in-memory
ledger continues to use the existing safe text fallback rather than inventing
or persisting reconstructed native evidence.

## Selected Interaction Model

The child page inherits the parent UI's `getToolsExpanded()` value when the
page opens. The `app.tools.expand` binding toggles a page-local value while
the child page is focused. It does not call `setToolsExpanded()` on the parent
UI, so inspecting a child cannot unexpectedly expand or collapse the parent
conversation after the overlay closes. The page footer displays the configured
binding and the current expand/collapse action.

This uses the same effective compact/full setup semantics as the parent:
existing wrapped tool definitions determine the compact default, and the
standard Pi expansion binding reveals native evidence. No setup setting or
command is added.

## Architecture

`AgentSessionPage` owns only local interaction state. It passes `expanded` to
the shared `AgentTranscriptRenderer`, which forwards it for every tool block
to `AgentToolRenderer`. `AgentToolRenderLedger` applies the value to the
existing Pi `ToolExecutionComponent` before rendering.

Both entry points supply the initial state through the shared page:

- Direct Subagent takeover reads the parent UI state when it opens the
takeover overlay.
- Workflow dashboard captures that state when the dashboard overlay opens and
passes it to its transcript page.

The Direct and Workflow document adapters remain projections into the same
renderer. There is no duplicate formatter or child-specific tool definition.

## Validation

Focused tests prove that a wrapped Bash child call is one compact row by
default, reveals its command and output through the native renderer when the
configured expansion binding is used, and returns to the compact row. Existing
activity-renderer tests cover pending, success, error, long output, narrow
width, and all supported tool kinds. Direct and Workflow parity is asserted
against the same renderer output.

The repository gates are `bun run check` and `bun run test`. Manual Pi smoke
covered Direct Subagent and Workflow child pages, compact and expanded output,
parent-state inheritance, page-local toggling, and Nerd Font icons.
1 change: 1 addition & 0 deletions docs/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ These records predate [`Decision 0001`](../decisions/0001-documentation-and-evid
- [`PI_COMMUNITY_PACKAGE_AUDIT_2026-08-09.md`](PI_COMMUNITY_PACKAGE_AUDIT_2026-08-09.md) — code-grounded comparison of leading community packages
- [`OPENPI_WORKFLOW_V2_DESIGN_2026-08-23.md`](OPENPI_WORKFLOW_V2_DESIGN_2026-08-23.md) — Pi-native Workflow interface and lifecycle redesign with implementation evidence
- [`WORKFLOW_INVOCATION_GRAPH.md`](WORKFLOW_INVOCATION_GRAPH.md) — durable invocation facts, same-run handoff refs, reusable operators, and derived graph semantics
- [`CHILD_TOOL_ACTIVITY.md`](CHILD_TOOL_ACTIVITY.md) — shared compact and Pi-native expanded evidence projection for Direct Subagent and Workflow child transcripts
18 changes: 18 additions & 0 deletions extensions/shared/agent-session-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export interface AgentSessionPageSource {
abort?(): void;
}

export interface AgentSessionPageOptions {
/** Inherited from the parent UI when the child page opens. */
readonly toolsExpanded?: boolean;
}

function safeLine(value: string) {
return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
}
Expand Down Expand Up @@ -76,6 +81,7 @@ export class AgentSessionPage implements Component, Focusable {
private viewport = new TranscriptViewport();
private rowCount = 0;
private viewportSize = 1;
private toolsExpanded: boolean;

private _focused = false;
get focused() {
Expand All @@ -90,15 +96,22 @@ export class AgentSessionPage implements Component, Focusable {
theme: Theme,
keybindings: KeybindingsManager,
source: AgentSessionPageSource,
options?: AgentSessionPageOptions,
) {
this.tui = tui;
this.theme = theme;
this.keybindings = keybindings;
this.source = source;
this.toolsExpanded = options?.toolsExpanded === true;
}

handleInput(data: string) {
const state = this.source.getState();
if (this.keybindings.matches(data, "app.tools.expand")) {
this.toolsExpanded = !this.toolsExpanded;
this.tui.requestRender();
return;
}
if (
this.source.abort &&
state?.status === "running" &&
Expand Down Expand Up @@ -229,6 +242,7 @@ export class AgentSessionPage implements Component, Focusable {
const transcriptCapacity = Math.max(1, bodyHeight - errorRows);
const transcript = this.renderer.render(state.document, width, this.theme, {
now,
expanded: this.toolsExpanded,
});
this.rowCount = transcript.length;
this.viewportSize = transcriptCapacity;
Expand Down Expand Up @@ -282,6 +296,10 @@ export class AgentSessionPage implements Component, Focusable {
const hints: ScreenHint[] = [];
hints.push([keys("app.interrupt"), "back"]);
if (this.source.abort) hints.push([keys("app.clear"), "abort run"]);
hints.push([
keys("app.tools.expand"),
this.toolsExpanded ? "collapse tools" : "expand tools",
]);
hints.push(
[
`${keys("tui.editor.cursorUp")}/${keys("tui.editor.cursorDown")}`,
Expand Down
10 changes: 10 additions & 0 deletions extensions/shared/agent-tool-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface AgentToolRenderRequest {
readonly toolId: string;
readonly name: string;
readonly cwd?: string;
/** The child page's local Pi-native evidence visibility. */
readonly expanded?: boolean;
}

/** Operator-only projection. It is deliberately absent from persisted state. */
Expand All @@ -34,6 +36,7 @@ interface ToolExecutionRecord {
isPartial: boolean;
component?: ToolExecutionComponent;
componentCwd?: string;
componentExpanded?: boolean;
}

const inertTui = {
Expand Down Expand Up @@ -99,6 +102,7 @@ export class AgentToolRenderLedger implements AgentToolRenderer {
current.definition = definition;
current.component = undefined;
current.componentCwd = undefined;
current.componentExpanded = undefined;
}
current.name = name;
if (current.args !== args) {
Expand Down Expand Up @@ -190,13 +194,19 @@ export class AgentToolRenderLedger implements AgentToolRenderer {
cwd,
);
execution.componentCwd = cwd;
execution.componentExpanded = false;
if (execution.executionStarted)
execution.component.markExecutionStarted();
if (execution.argsComplete) execution.component.setArgsComplete();
if (execution.result) {
execution.component.updateResult(execution.result, execution.isPartial);
}
}
const expanded = request.expanded === true;
if (execution.componentExpanded !== expanded) {
execution.component.setExpanded(expanded);
execution.componentExpanded = expanded;
}
return execution.component.render(width);
}

Expand Down
18 changes: 16 additions & 2 deletions extensions/shared/agent-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,12 @@ function renderToolBlock(
now: number,
cwd?: string,
toolRenderer?: AgentToolRenderer,
expanded = false,
) {
const native = toolRenderer?.renderTool({ toolId, name, cwd }, width);
const native = toolRenderer?.renderTool(
{ toolId, name, cwd, expanded },
width,
);
if (native) return native;
const { args, fallback } = parseToolArgsPreview(argsPreview);
return [
Expand Down Expand Up @@ -194,6 +198,7 @@ function renderAssistantItem(
now: number,
cwd?: string,
toolRenderer?: AgentToolRenderer,
expanded = false,
) {
const out = renderAssistantParts(item.parts, width);
for (const part of item.parts) {
Expand All @@ -215,6 +220,7 @@ function renderAssistantItem(
now,
cwd,
toolRenderer,
expanded,
),
);
}
Expand All @@ -230,6 +236,7 @@ function renderToolResultItem(
now: number,
cwd?: string,
toolRenderer?: AgentToolRenderer,
expanded = false,
) {
if (paired) return [];
return renderToolBlock(
Expand All @@ -243,6 +250,7 @@ function renderToolResultItem(
now,
cwd,
toolRenderer,
expanded,
);
}

Expand Down Expand Up @@ -294,6 +302,7 @@ function renderTranscriptItem(
now: number,
cwd?: string,
toolRenderer?: AgentToolRenderer,
expanded = false,
) {
if (item.kind === "user") return renderUserText(item.text, width);
if (item.kind === "assistant") {
Expand All @@ -305,6 +314,7 @@ function renderTranscriptItem(
now,
cwd,
toolRenderer,
expanded,
);
}
return renderToolResultItem(
Expand All @@ -315,6 +325,7 @@ function renderTranscriptItem(
now,
cwd,
toolRenderer,
expanded,
);
}

Expand Down Expand Up @@ -410,10 +421,11 @@ export class AgentTranscriptRenderer {
document: AgentTranscriptDocument,
width: number,
theme: Theme,
options?: { readonly now?: number },
options?: { readonly now?: number; readonly expanded?: boolean },
) {
const out: string[] = [];
const now = options?.now ?? Date.now();
const expanded = options?.expanded === true;
const liveTools = document.liveTools ?? [];
if (document.toolRenderer) this.toolRenderers.add(document.toolRenderer);
const liveIds = new Set(liveTools.map((tool) => tool.toolId));
Expand All @@ -435,6 +447,7 @@ export class AgentTranscriptRenderer {
now,
document.cwd,
document.toolRenderer,
expanded,
);
if (!cached && cacheable) {
const widths = this.itemCache.get(item) ?? new Map<string, string[]>();
Expand Down Expand Up @@ -481,6 +494,7 @@ export class AgentTranscriptRenderer {
now,
document.cwd,
document.toolRenderer,
expanded,
),
);
}
Expand Down
68 changes: 44 additions & 24 deletions extensions/subagents/src/ui/takeover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ function statusGlyph(

export interface TakeoverOptions {
readonly badge?: string;
/** Captured from the parent UI when this child page opens. */
readonly toolsExpanded?: boolean;
}

export async function openSubagentTakeover(
Expand All @@ -75,9 +77,21 @@ export async function openSubagentTakeover(
options?: TakeoverOptions,
) {
if (!view.get(id)) return;
const takeoverOptions: TakeoverOptions = {
...options,
toolsExpanded: ctx.ui.getToolsExpanded(),
};
await ctx.ui.custom<null>(
(tui, theme, keybindings, done) =>
new TakeoverView(tui, theme, keybindings, id, view, done, options),
new TakeoverView(
tui,
theme,
keybindings,
id,
view,
done,
takeoverOptions,
),
{
overlay: true,
overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" },
Expand Down Expand Up @@ -407,30 +421,36 @@ export class TakeoverView implements Component, Focusable {
done: (value: null) => void,
options?: TakeoverOptions,
) {
this.page = new AgentSessionPage(tui, theme, keybindings, {
getState: () => {
const snap = view.get(id);
if (!snap) return undefined;
return {
id: snap.id,
title: snap.title,
status: snap.status,
document: subagentTranscriptDocument(
snap,
view.getToolRenderer?.(id),
),
metadata: [
options?.badge,
snap.meta.modelLabel,
formatContextUtilization(snap.usage),
formatElapsed(snap),
],
errorText: snap.errorText,
};
this.page = new AgentSessionPage(
tui,
theme,
keybindings,
{
getState: () => {
const snap = view.get(id);
if (!snap) return undefined;
return {
id: snap.id,
title: snap.title,
status: snap.status,
document: subagentTranscriptDocument(
snap,
view.getToolRenderer?.(id),
),
metadata: [
options?.badge,
snap.meta.modelLabel,
formatContextUtilization(snap.usage),
formatElapsed(snap),
],
errorText: snap.errorText,
};
},
close: () => this.close(done),
abort: () => view.requestAbort(id),
},
close: () => this.close(done),
abort: () => view.requestAbort(id),
});
{ toolsExpanded: options?.toolsExpanded },
);
this.unsubscribe = view.subscribeTo(id, () => {
this.refreshTicker(view.get(id), tui);
this.scheduleRender(tui);
Expand Down
Loading
Loading