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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,15 +453,15 @@ pi install npm:pi-intercom
<details>
<summary><strong>模型工具速查</strong></summary>

Capability discovery 默认是 `explicit`:普通父 Session 不常驻任何 OpenPI 模型工具,首轮保持 Pi 原生 `read`、`bash`、`edit`、`write`。用户明确要求结构化搜索、Subagent、Workflow、后台进程或 Session Goal/Tasks 时,OpenPI 在 `before_agent_start` 直接加载对应能力组;明确询问 OpenPI capabilities/tools/features 时显示 `openpi_load_tools`。可通过 `/openpi-setup` 显式选择 `adaptive`:此时只让小型 `openpi_load_tools` 网关常驻,模型可在判断任务确实受益时自主加载一个能力组。该选择也授权模型启动该组内的昂贵工作,因此不作为默认值。条件句(例如 “If you delegate…”)不会被当成显式委派意图。能力组在当前 Session 内单调保持,避免反复增删工具破坏缓存。组内管理工具仍只在资源成功创建或状态确实存在后出现。Mode / Setup / Context 工具独立跟随实时状态显示和隐藏。Background、Subagent 与 Workflow 的 Skill 文件仍随包发布,但只在对应能力触发后提示读取,不常驻普通系统 Prompt。
Capability discovery 默认是 `explicit`:普通父 Session 不常驻任何 OpenPI 模型工具,首轮保持 Pi 原生 `read`、`bash`、`edit`、`write`。用户明确要求结构化搜索、Subagent、Workflow、后台进程或 Session Goal/Tasks 时,OpenPI 在 `before_agent_start` 直接加载对应能力组;明确询问 OpenPI capabilities/tools/features 时显示 `openpi_load_tools`。可通过 `/openpi-setup` 显式选择 `adaptive`:此时只让小型 `openpi_load_tools` 网关常驻,模型可在判断任务确实受益时自主加载一个能力组。该选择也授权模型启动该组内的昂贵工作,因此不作为默认值。条件句(例如 “If you delegate…”)不会被当成显式委派意图。能力组在当前 Session 内单调保持,避免反复增删工具破坏缓存。Delegate 一经加载便一次性开放完整、稳定的 Subagent 工具族;资源不存在时由工具执行层明确返回空状态或 fail-closed,而不再按实例生命周期改变模型接口。其他组内管理工具仍只在资源成功创建或状态确实存在后出现。Mode / Setup / Context 工具独立跟随实时状态显示和隐藏。Background、Subagent 与 Workflow 的 Skill 文件仍随包发布,但只在对应能力触发后提示读取,不常驻普通系统 Prompt。

普通产品默认采用 Pi-native execution:保留 Pi 原生完整历史、工具输出上限、Session compaction、显式 Bash timeout 与 provider loop,不再额外做固定事务投影、成功 Bash 二次裁剪、测试 timeout 改写、重复失败硬拦或恢复/轨迹提示。OpenPI 只保留独立的工作区安全边界:阻止未授权删除 pre-existing 路径,并从实际文件状态识别本轮通过原生写入、文字重定向或 literal `mkdir -p` 创建的 scratch,避免误拦其清理。旧执行策略仅保留为受 benchmark root 门控的实验 profile,不会进入普通 Session。

| 工具 | 用途 | 可见时机 |
| -------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------- |
| `openpi_load_tools` | 列出或加载可选工具组 | 明确询问;或启用 `adaptive` |
| `bg_start`, `bg_status`, `bg_list`, `bg_watch`, `bg_kill` | 后台进程生命周期 | 明确意图或 adaptive;启动后展开 |
| `subagent_spawn`, `subagent_check`, `subagent_list`, `subagent_wait`, `subagent_send`, `subagent_cancel` | 独立子 Agent | 明确意图或 adaptive;创建后展开 |
| `subagent_spawn`, `subagent_check`, `subagent_list`, `subagent_wait`, `subagent_send`, `subagent_cancel` | 独立子 Agent | 明确意图或 adaptive;整组稳定加载 |
| `workflow`, `workflow_status`, `workflow_stop` | 动态多阶段编排与运行管理 | 明确意图或 adaptive;运行后展开 |
| `tasks_add`, `tasks_update`, `tasks_list` | Session 工作项 | 明确意图或 adaptive;存在后展开 |
| `get_goal`, `create_goal`, `update_goal` | Session Goal | 明确意图或 adaptive;存在后展开 |
Expand Down
44 changes: 30 additions & 14 deletions extensions/ask-user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,37 @@ import type {
import {
Editor,
type EditorTheme,
type Focusable,
Key,
matchesKey,
Text,
truncateToWidth,
type Focusable,
wrapTextWithAnsi,
} from "@earendil-works/pi-tui";
import { Cause, Effect, Exit } from "effect";
import { Type, type Static } from "typebox";
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
import { type Static, Type } from "typebox";
import {
PLAN_MODE_CHANNEL,
type PlanModeState,
} from "../shared/plan-mode-state.ts";
import { hintLine } from "../shared/screen-chrome.ts";
import {
OPENPI_SETUP_EPISODE_CHANNEL,
type OpenPiSetupEpisodeState,
} from "../shared/setup-episode-state.ts";
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
import {
OPENPI_TOOL_SURFACE,
patchOwnedTools,
} from "../shared/tool-surface.ts";
import { createHumanHandoffToolDefinition } from "./handoff.ts";
import {
BRACKETED_PASTE_END,
BRACKETED_PASTE_START,
MAX_ANSWER_DRAFT_UTF8_BYTES,
answerDraftByteLength,
answerDraftFits,
BRACKETED_PASTE_END,
BRACKETED_PASTE_START,
longerThanAnswerDraftLimit,
MAX_ANSWER_DRAFT_UTF8_BYTES,
prospectiveAnswerDraftFits,
sanitizeAnswerDraftEditorInput,
} from "./limits.ts";
Expand All @@ -63,7 +64,7 @@ const MAX_OPTIONS = 5;
/** Preview lines rendered before the tail is summarized. */
const PREVIEW_MAX_LINES = 20;

export { MAX_ANSWER_DRAFT_UTF8_BYTES, answerDraftFits } from "./limits.ts";
export { answerDraftFits, MAX_ANSWER_DRAFT_UTF8_BYTES } from "./limits.ts";

const OptionSchema = Type.Object({
label: Type.String({
Expand Down Expand Up @@ -949,9 +950,15 @@ export default function askUser(pi: ExtensionAPI) {
);
lines.push("");
add(
theme.fg(
"dim",
` ↑↓ choose • 1-${params.questions.length} edit answer • Enter open/submit • Esc dismiss`,
hintLine(
theme,
[
["↑↓", "choose"],
[`1-${params.questions.length}`, "edit answer"],
["enter", "open/submit"],
["esc", "dismiss"],
],
width,
),
);
add(theme.fg("accent", "─".repeat(width)));
Expand Down Expand Up @@ -1043,11 +1050,20 @@ export default function askUser(pi: ExtensionAPI) {

lines.push("");
add(
theme.fg(
"dim",
hintLine(
theme,
editMode
? " Enter save answer • Esc keep draft and return"
: ` ↑↓ or 1-${currentOptions.length} select • Tab add notes • Enter save draft answer • Esc dismiss`,
? [
["enter", "save answer"],
["esc", "keep draft and return"],
]
: [
[`↑↓ or 1-${currentOptions.length}`, "select"],
["tab", "add notes"],
["enter", "save draft answer"],
["esc", "dismiss"],
],
width,
),
);
add(theme.fg("accent", "─".repeat(width)));
Expand Down
193 changes: 86 additions & 107 deletions extensions/background-terminals/src/ui/ps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import type {
import { formatSize } from "@earendil-works/pi-coding-agent";
import type { Component, TUI } from "@earendil-works/pi-tui";
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
import {
hintLine,
overflowNote,
panelFrame,
screenTitleLine,
} from "../../../shared/screen-chrome.ts";
import {
formatDuration,
formatElapsed,
Expand Down Expand Up @@ -217,24 +223,6 @@ class TerminalDashboard implements Component {
}
}

private pad(text: string, width: number): string {
const truncated = truncateToWidth(text, width);
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
}

private borderSegment(width: number, title: string): string {
const theme = this.theme;
const label = title
? ` ${truncateToWidth(title, Math.max(0, width - 3))} `
: "";
const labelWidth = visibleWidth(label);
return (
theme.fg("border", "─") +
(label ? theme.fg("text", label) : "") +
theme.fg("border", "─".repeat(Math.max(0, width - 1 - labelWidth)))
);
}

render(width: number): string[] {
const theme = this.theme;
const terminals = this.terminals();
Expand All @@ -245,64 +233,34 @@ class TerminalDashboard implements Component {
// chat, editor, and extra footer lines while leaving pi's final footer
// row visible.
const bodyHeight = Math.max(6, rows - 5);
const innerWidth = width - 2;

const lines: string[] = [];

// Header: title left, count right
const headerLeft = theme.fg("accent", theme.bold("Background terminals"));
const headerRight = theme.fg(
"muted",
`${terminals.length} terminal${terminals.length === 1 ? "" : "s"}`,
);
const headerPad = Math.max(
1,
width - visibleWidth(headerLeft) - visibleWidth(headerRight) - 4,
);
lines.push(
truncateToWidth(
` ${headerLeft}${" ".repeat(headerPad)}${headerRight} `,
const running = terminals.filter((s) => s.status === "running").length;
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
configuredKeys(this.keybindings, binding);

return [
screenTitleLine(
theme,
"Background terminals",
`${terminals.length} terminal${terminals.length === 1 ? "" : "s"}`,
width,
),
);

// Top border with panel title
const running = terminals.filter((s) => s.status === "running").length;
lines.push(
theme.fg("border", "╭") +
this.borderSegment(
innerWidth,
`terminals · ${running} running / ${terminals.length}`,
) +
theme.fg("border", "╮"),
);

// Rows
const divider = theme.fg("border", "│");
const rowLines = this.renderRows(terminals, innerWidth, bodyHeight);
for (let i = 0; i < bodyHeight; i++) {
lines.push(divider + this.pad(rowLines[i] ?? "", innerWidth) + divider);
}

// Bottom border
lines.push(
theme.fg("border", "╰") +
theme.fg("border", "─".repeat(Math.max(0, innerWidth))) +
theme.fg("border", "╯"),
);

// Hints
lines.push(
truncateToWidth(
theme.fg(
"dim",
` ${configuredKeys(this.keybindings, "tui.select.up")}/${configuredKeys(this.keybindings, "tui.select.down")}/jk select · ${configuredKeys(this.keybindings, "tui.select.confirm")} inspect · x kill · ${configuredKeys(this.keybindings, "tui.select.cancel")} close`,
),
...panelFrame(theme, {
label: `terminals · ${running}/${terminals.length} running`,
rows: this.renderRows(terminals, width - 2, bodyHeight),
width,
height: bodyHeight + 2,
}),
hintLine(
theme,
[
[`${keys("tui.select.up")}/${keys("tui.select.down")}/jk`, "select"],
[keys("tui.select.confirm"), "inspect"],
["x", "kill"],
[keys("tui.select.cancel"), "close"],
],
width,
),
);

return lines;
];
}

private renderRows(
Expand Down Expand Up @@ -359,13 +317,13 @@ class TerminalDashboard implements Component {
out.push(truncateToWidth(leftTruncated + " ".repeat(gap) + right, width));
}

if (start > 0) {
out[0] = truncateToWidth(theme.fg("dim", ` ... ${start} more`), width);
}
if (start > 0) out[0] = overflowNote(theme, start, width, "above");
if (start + height < terminals.length) {
out[out.length - 1] = truncateToWidth(
theme.fg("dim", ` ... ${terminals.length - start - height} more`),
out[out.length - 1] = overflowNote(
theme,
terminals.length - start - height,
width,
"below",
);
}
return out;
Expand Down Expand Up @@ -514,43 +472,51 @@ class TerminalDetailView implements Component {

render(width: number): string[] {
const theme = this.theme;
const border = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
// One accent rule opens and closes the overlay; interior seams stay quiet
// so the output, not the frame, is what the eye lands on.
const edge = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
const seam = theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
const lines: string[] = [];
const snap = this.snap();

if (!snap) {
lines.push(border);
lines.push(edge);
lines.push(theme.fg("dim", `${this.id} is no longer tracked`));
lines.push(border);
lines.push(edge);
return lines;
}

lines.push(border);
const header =
lines.push(edge);
const dot = theme.fg("dim", " · ");
const header = [
`${statusGlyph(snap, theme)} ` +
theme.fg("accent", theme.bold(`${snap.id} · ${oneLine(snap.title)}`)) +
theme.fg(
"muted",
` · ${snap.status} · ${formatElapsed(snap)} · pid ${snap.pid ?? "?"}`,
) +
(snap.status !== "running"
? theme.fg("muted", ` · ${formatExit(snap)}`)
: "") +
(snap.status === "running" && snap.timeoutAt !== undefined
? theme.fg(
"warning",
` · ${formatDuration((snap.timeoutAt - Date.now()) / 1_000)} remaining`,
)
: "") +
theme.fg("dim", ` · ${snap.cwd}`);
theme.fg("accent", theme.bold(`${snap.id} · ${oneLine(snap.title)}`)),
statusWord(snap, theme),
theme.fg("muted", formatElapsed(snap)),
theme.fg("muted", `pid ${snap.pid ?? "?"}`),
...(snap.status !== "running"
? [theme.fg("muted", formatExit(snap))]
: []),
...(snap.status === "running" && snap.timeoutAt !== undefined
? [
theme.fg(
"warning",
`${formatDuration((snap.timeoutAt - Date.now()) / 1_000)} left`,
),
]
: []),
theme.fg("dim", snap.cwd),
]
.filter(Boolean)
.join(dot);
lines.push(truncateToWidth(header, width));
lines.push(
truncateToWidth(
theme.fg("dim", "$ ") + theme.fg("text", oneLine(snap.command)),
width,
),
);
lines.push(border);
lines.push(seam);

// Stream tab line: which stream is active, both sizes.
const active = this.stream;
Expand All @@ -561,7 +527,7 @@ class TerminalDetailView implements Component {
: theme.fg("dim", `${name} (${formatSize(size)})`);
lines.push(
truncateToWidth(
` ${tab("stdout", snap.stdout.totalBytes)}${theme.fg("dim", " | ")}${tab("stderr", snap.stderr.totalBytes)}${theme.fg("dim", " — t to switch")}`,
` ${tab("stdout", snap.stdout.totalBytes)}${theme.fg("dim", " · ")}${tab("stderr", snap.stderr.totalBytes)}${theme.fg("dim", " t")} ${theme.fg("dim", "switch")}`,
width,
),
);
Expand Down Expand Up @@ -616,25 +582,38 @@ class TerminalDetailView implements Component {
if (this.scrollOffset > 0) {
body.push(
truncateToWidth(
theme.fg("dim", `... ${this.scrollOffset} lines below · ↓/pgdn`),
theme.fg("dim", ` ${this.scrollOffset} lines below · ↓/pgdn`),
width,
),
);
}
while (body.length < viewport) body.push("");
lines.push(...body.slice(0, viewport));

lines.push(border);
lines.push(seam);
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
configuredKeys(this.keybindings, binding);
lines.push(
truncateToWidth(
theme.fg(
"dim",
`${configuredKeys(this.keybindings, "tui.select.cancel")} back · t stdout/stderr · x kill · ${configuredKeys(this.keybindings, "tui.editor.cursorUp")}/${configuredKeys(this.keybindings, "tui.editor.cursorDown")}/jk scroll · ${configuredKeys(this.keybindings, "tui.editor.pageUp")}/${configuredKeys(this.keybindings, "tui.editor.pageDown")} page · g/G top/bottom`,
),
hintLine(
theme,
[
[keys("tui.select.cancel"), "back"],
["t", "stdout/stderr"],
["x", "kill"],
[
`${keys("tui.editor.cursorUp")}/${keys("tui.editor.cursorDown")}/jk`,
"scroll",
],
[
`${keys("tui.editor.pageUp")}/${keys("tui.editor.pageDown")}`,
"page",
],
["g/G", "top/bottom"],
],
width,
),
);
lines.push(border);
lines.push(edge);
return lines;
}

Expand Down
Loading
Loading