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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ pi install npm:@tt-a1i/openpi
OpenPI 会把长期进程放到后台,把独立任务交给隔离 Context 的 Pi Subagent,把多阶段依赖组织成 Workflow。状态会持续显示;完整运行可从 `/ps`、`/subagents` 和 `/workflows` 检查或终止。

> [!TIP]
> Capability discovery 默认 `explicit`:明确说出能力意图才会加载对应组。
> 例如「在后台运行 dev server」→ 后台终端;「用/使用子代理检查」→ Subagent;「用工作流编排」→ Workflow;「用 fd/rg 搜索」或「用 git diff 比较分支」→ 搜索与只读 Git 工具。
> Capability discovery 默认 `explicit`:明确说出能力意图才会加载对应组。英文 `subagent` 与 `workflow` 是保留授权词,单独输入也会加载对应能力。
> 例如 `subagent, workflow` → 同时加载两组;「在后台运行 dev server」→ 后台终端;「用/使用子代理检查」→ Subagent;「用工作流编排」→ Workflow;「用 fd/rg 搜索」或「用 git diff 比较分支」→ 搜索与只读 Git 工具。
> 关键是把意图说清楚(说「用子代理」「后台运行」这类带动作的短语),不需要记住任何工具名。
> 在交互输入框中,保留词 `Subagent` / `Workflow`,以及已被识别的中文能力请求,会使用 Claude Code 风格的薰衣草紫显示;浅色终端自动使用更深的紫色以维持可读性。变色表示提交后会加载对应能力。因为英文名称本身就是授权词,讨论中写出它们也会开闸;条件句和否定句仍保持普通显示,Suggestion 幽灵文字也要在用户接受进输入框后才参与识别。

> [!IMPORTANT]
> 默认安装是安静的:不改主题、不绑定 Provider 或模型、不开启下一步预测,也不执行 post-edit 命令。Capability discovery 默认 `explicit`;只有用户通过 `/openpi-setup` 选择 `adaptive` 后,模型才会常驻看到一个小型发现网关并可自主加载额外能力。
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 plain layout (`cwd git pr |flex| model context`; `thinking`, `cache`, `cost`, and `throughput` remain opt-in metrics). 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 affects powerline separator glyphs (``) and adds Codex-style outline icons to compact read, terminal, edit, search, and directory activity rows; all accompanying text remains readable without it. Metrics carry small built-in Unicode icons (`📁` directory, `⎇` branch, `✦` model, and a `◔◑◕●` context gauge), all of which render in standard terminal fonts. 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). Ordinary `read`, `grep`, `find`, and `ls` operations render as one-line semantic activity summaries. Bash and Write/Edit default to the same activity-row projection, showing the target, running/success/failure state, and useful line or diff counts without replaying stdout or previews. Select full independently for Bash or Write/Edit to keep Pi's native rendering expanded. `app.tools.expand` temporarily restores the native arguments, output, errors, diff, timing, images, truncation notices, and full-output metadata; it never changes Session history or model context. 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. The case-insensitive English words `subagent` and `workflow` are reserved authorization words: entering either word is sufficient to load its capability group, and the interactive editor shows it in Claude Code-style lavender before submission, with a darker purple fallback for light themes. This makes discussion that contains either English word an intentional opt-in tradeoff; conditional and negated clauses remain inert, while Chinese capability names still require a recognized action request. `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. Accepted Suggestion text is classified only after it becomes real editor input. The visual feedback and runtime activation share one fail-closed intent classifier. 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 plain layout (`cwd git pr |flex| model context`; `thinking`, `cache`, `cost`, and `throughput` remain opt-in metrics). 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 affects powerline separator glyphs (``) and adds Codex-style outline icons to compact read, terminal, edit, search, and directory activity rows; all accompanying text remains readable without it. Metrics carry small built-in Unicode icons (`📁` directory, `⎇` branch, `✦` model, and a `◔◑◕●` context gauge), all of which render in standard terminal fonts. 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). Ordinary `read`, `grep`, `find`, and `ls` operations render as one-line semantic activity summaries. Bash and Write/Edit default to the same activity-row projection, showing the target, running/success/failure state, and useful line or diff counts without replaying stdout or previews. Select full independently for Bash or Write/Edit to keep Pi's native rendering expanded. `app.tools.expand` temporarily restores the native arguments, output, errors, diff, timing, images, truncation notices, and full-output metadata; it never changes Session history or model context. 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
30 changes: 25 additions & 5 deletions extensions/capabilities/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import type {
ExtensionAPI,
ExtensionContext,
} from "@earendil-works/pi-coding-agent";
import { SETUP_CONFIG_CHANGED_CHANNEL } from "../shared/setup-config.ts";
import {
OPENPI_TOOL_SURFACE,
Expand Down Expand Up @@ -56,7 +59,7 @@ function harness(options: { discovery?: "explicit" | "adaptive" } = {}) {
];
let active = [...available];
const tools = new Map<string, CapturedTool>();
const starts: (() => void)[] = [];
const starts: Array<(event: unknown, ctx: ExtensionContext) => void> = [];
const beforeStarts: ((event: { prompt: string }) => unknown)[] = [];
const eventHandlers = new Map<string, Set<(data: unknown) => void>>();
const pi = {
Expand All @@ -75,7 +78,9 @@ function harness(options: { discovery?: "explicit" | "adaptive" } = {}) {
event: string,
handler: (() => void) | ((event: { prompt: string }) => unknown),
) {
if (event === "session_start") starts.push(handler as () => void);
if (event === "session_start") {
starts.push(handler as (event: unknown, ctx: ExtensionContext) => void);
}
if (event === "before_agent_start") {
beforeStarts.push(handler as (event: { prompt: string }) => unknown);
}
Expand Down Expand Up @@ -116,7 +121,8 @@ function harness(options: { discovery?: "explicit" | "adaptive" } = {}) {
return {
active: () => [...active],
start: () => {
for (const handler of starts) handler();
const ctx = { mode: "json" } as ExtensionContext;
for (const handler of starts) handler({}, ctx);
},
before: (prompt: string) => {
return beforeStarts.map((handler) => handler({ prompt }));
Expand Down Expand Up @@ -235,8 +241,22 @@ test("an explicit subagent request loads delegation directly", () => {
assert.match(JSON.stringify(results), /skills\/subagents\/SKILL\.md/);
});

test("reserved capability words load their groups without action verbs", () => {
const h = harness();
h.start();

h.before("subagent, workflow");

assert.ok(h.active().includes("subagent_spawn"));
assert.ok(h.active().includes("workflow"));
});

test("common Chinese and multi-agent delegation requests are explicit intent", () => {
for (const prompt of ["来多子代理一起讨论", "Use multiple subagents."]) {
for (const prompt of [
"来多子代理一起讨论",
"Use multiple subagents.",
"用 Subagent 检查这个实现",
]) {
const h = harness();
h.start();

Expand Down
70 changes: 29 additions & 41 deletions extensions/capabilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { fileURLToPath } from "node:url";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { type Static, Type } from "typebox";
import {
capabilitiesRequestedByPrompt,
requestsCapabilityGateway,
} from "../shared/capability-intent.ts";
import {
registerEditorLayer,
removeEditorLayer,
} from "../shared/editor-layers.ts";
import {
loadSetupConfig,
type MyPiSetupConfig,
Expand All @@ -16,6 +24,11 @@ import {
patchOwnedTools,
resetOpenPiToolSurface,
} from "../shared/tool-surface.ts";
import {
CapabilityIntentHighlightEditor,
colorCapabilityKeyword,
isLightNamedTheme,
} from "./src/ui.ts";

const CapabilitySchema = Type.Unsafe<OpenPiCapability>({
type: "string",
Expand All @@ -35,25 +48,6 @@ const OpenPiLoadToolsParameters = Type.Object({

type OpenPiLoadToolsInput = Static<typeof OpenPiLoadToolsParameters>;

const CAPABILITY_INTENT = {
search:
/\b(?:use|run)\s+(?:fd|rg)\b|\buse\s+(?:structured\s+)?(?:(?:file|code|content)\s+)?search\b|\b(?:structured|fast)\s+(?:file|code|content)\s+search\b|(?:使用|用|运行).{0,8}(?:fd|rg|git\s+(?:show|diff|log))|结构化(?:文件|代码|内容)搜索/iu,
delegate:
/\b(?:use|spawn|run)\s+(?:an?\s+|multiple\s+|several\s+|two\s+)?(?:pi\s+)?subagents?\b|(?:^|[.!?]\s+)(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\b(?:can|could|would)\s+you\s+(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\bparallel\s+agents?\b|(?:使用|用|启动|调用|来|开).{0,8}子代理|(?:多个?|多路)子代理|并行.{0,8}(?:代理|agent)|委派.{0,6}(?:任务|给|出去)/iu,
workflow:
/\b(?:use|run|create|build)\s+(?:(?:an?|the)\s+)?(?:openpi\s+)?workflow\b|(?:使用|用|运行|创建|构建).{0,8}工作流|用.{0,4}workflow/iu,
background:
/\b(?:run|start|keep)\b.{0,40}\b(?:in the background|background\s+(?:process|terminal|job))\b|后台.{0,8}(?:运行|启动|进程|终端|任务)/iu,
session:
/\b(?:create|set|update|track)\s+(?:an?\s+)?(?:session\s+)?(?:goal|task list|tasks)\b|(?:设置|创建|更新|跟踪|追踪).{0,8}(?:目标|任务)/iu,
} as const satisfies Record<OpenPiCapability, RegExp>;

const CAPABILITY_GATEWAY_INTENT =
/\bopenpi\s+(?:capabilit(?:y|ies)|tools?|features?)\b|openpi.{0,8}(?:能力|工具|功能)/iu;

const CONDITIONAL_OR_NEGATED_INTENT =
/^(?:\s*(?:only\s+)?(?:if|when|unless|before|in case)\b)|\b(?:do not|don't|cannot|can't|not|no|never|avoid)\b|\b(?:if|unless)\b|\bwhen\s+(?:needed|required|necessary)\b|(?:如果|若|假如|除非|仅当|需要时|不要|不能|不用|不必|无需|避免|请勿|禁止)/iu;

const CAPABILITY_SKILLS: Partial<Record<OpenPiCapability, string>> = {
delegate: fileURLToPath(
new URL("../../skills/subagents/SKILL.md", import.meta.url),
Expand All @@ -66,27 +60,6 @@ const CAPABILITY_SKILLS: Partial<Record<OpenPiCapability, string>> = {
),
};

function capabilitiesRequestedByPrompt(prompt: string) {
const clauses = prompt.split(/[\n.!?。!?;;]+/u);
return OPENPI_CAPABILITY_NAMES.filter((capability) =>
clauses.some(
(clause) =>
!CONDITIONAL_OR_NEGATED_INTENT.test(clause) &&
CAPABILITY_INTENT[capability].test(clause),
),
);
}

function requestsCapabilityGateway(prompt: string) {
return prompt
.split(/[\n.!?。!?;;]+/u)
.some(
(clause) =>
!CONDITIONAL_OR_NEGATED_INTENT.test(clause) &&
CAPABILITY_GATEWAY_INTENT.test(clause),
);
}

function capabilitySkillPaths(capabilities: readonly OpenPiCapability[]) {
return capabilities.flatMap((capability) => {
const skill = CAPABILITY_SKILLS[capability];
Expand Down Expand Up @@ -124,14 +97,29 @@ export function createCapabilitiesExtension(

pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, reconcileDiscoveryGateway);

pi.on("session_start", () => {
pi.on("session_start", (_event, ctx) => {
resetOpenPiToolSurface(
pi,
dependencies.sourcePath
? { capabilities: dependencies.sourcePath }
: undefined,
);
reconcileDiscoveryGateway();
registerEditorLayer(pi, ctx, {
id: "capability-intent-highlight",
order: 150,
wrap: (base, _tui, _theme, keybindings) =>
new CapabilityIntentHighlightEditor(base, keybindings, (text) =>
colorCapabilityKeyword(text, {
colorMode: ctx.ui.theme.getColorMode(),
light: isLightNamedTheme(ctx.ui.theme.name),
}),
),
});
});

pi.on("session_shutdown", () => {
removeEditorLayer(pi, "capability-intent-highlight");
});

pi.on("before_agent_start", (event) => {
Expand Down
Loading
Loading