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
15 changes: 10 additions & 5 deletions docs/user/providers-pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ reporting while a response streams and after it settles.
Pi skills appear in the composer's `$` menu. This includes user skills and project skills that Pi
loads for the current workspace; selecting one uses Pi's native skill expansion.

Pi loads its normal user and project extensions. Blocking `select`, `confirm`, `input`, and `editor`
dialogs work in T3 Code. Notifications appear in the work log. Pi terminal decoration such as
titles, status lines, and widgets does not have a T3 Code equivalent.
Pi loads its normal user extensions, and project extensions once the folder is trusted in Pi.
Blocking `select`, `confirm`, `input`, and `editor` dialogs work in T3 Code. Notifications appear in
the work log. Pi terminal decoration such as titles, status lines, and widgets does not have a T3
Code equivalent.

## Permission Modes

Expand Down Expand Up @@ -79,7 +80,11 @@ threads.
- If no models appear, open Pi directly and confirm its authentication and model configuration.
- If discovery cannot complete, T3 Code keeps Pi available with the `Pi default` model. Start a
thread to let the interactive Pi session handle any startup prompt.
- If a project extension is missing, approve the project in Pi, then start a fresh provider session.
- If a project skill is missing from the `$` menu, approve the project in Pi and refresh the provider.
- If Pi skips project settings, extensions, skills, prompts, MCP servers, or system prompt files
because the folder is not trusted, the thread shows a notice on the first turn of the Pi session.
Run the configured Pi binary in that folder on the server machine, using the same provider
environment, including `PI_CODING_AGENT_DIR` if set. Trust the folder or its parent with `/trust`,
then start a new thread using that same worktree. Launch arguments with `--no-approve` keep
project files off without the notice.
- The context meter appears once Pi reports usage for the thread. Some model providers only
report usage when a response completes, so the meter can wait for the first reply.
33 changes: 31 additions & 2 deletions packages/provider-pi/src/server/mcpExtensionSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ type InputHook = (
ctx: { ui: { notify: (message: string, level: string) => void } },
) => Promise<{ action: string; text: string; images?: ReadonlyArray<unknown> } | undefined>;

async function loadHooks(commands: ReadonlyArray<SkillCommand> = []) {
async function loadHooks(
commands: ReadonlyArray<SkillCommand> = [],
argv: ReadonlyArray<string> = [],
) {
const handlers = new Map<string, unknown>();
const source = NodeModule.stripTypeScriptTypes(
PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace(
Expand All @@ -34,7 +37,7 @@ async function loadHooks(commands: ReadonlyArray<SkillCommand> = []) {
),
);
await NodeVM.runInNewContext(`${source}\nt3McpExtension(pi)`, {
process: { env: {} },
process: { env: {}, argv },
NodeFSP,
NodePath,
stripFrontmatter: (content: string) => content,
Expand Down Expand Up @@ -77,6 +80,32 @@ describe("Pi orchestration prompt section", () => {
});
});

describe("Pi project trust", () => {
it.each([
{ name: "a trusted folder", trusted: true, argv: [], notices: 0 },
{ name: "an untrusted folder", trusted: false, argv: [], notices: 1 },
{ name: "--no-approve", trusted: false, argv: ["--approve", "-na"], notices: 0 },
])("sends $notices trust notice per session for $name", async ({ trusted, argv, notices }) => {
const sent: string[] = [];
const ctx = {
cwd: "/repo",
isProjectTrusted: () => trusted,
ui: { notify: (message: string) => sent.push(message) },
};
// Pi reruns extension factories on session replacement without asking about trust again.
for (const _session of [1, 2]) {
const handlers = (await loadHooks([], ["node", "pi", ...argv])) as Map<
string,
(event: unknown, ctx: unknown) => unknown
>;
for (const _turn of [1, 2]) await handlers.get("before_agent_start")!({}, ctx);
}
assert.equal(sent.length, notices * 2);
for (const message of sent)
assert.include(message, "for /repo because the folder is not trusted");
});
});

describe("Pi MCP tool results", () => {
it("returns mixed screenshot blocks and typed script output without repeating structured text", async () => {
const content = [
Expand Down
14 changes: 14 additions & 0 deletions packages/provider-pi/src/server/mcpExtensionSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,20 @@ function createMcpClient(endpoint: string, token: string) {
}

export default async function t3McpExtension(pi: ExtensionAPI) {
// RPC has no trust prompt, so Pi skips an untrusted folder's protected resources. Session
// replacement reruns this factory without project_trust, and T3 drops notices outside a turn,
// so check each session's first turn. An explicit --no-approve is deliberate and stays quiet.
let projectTrustChecked = false;
pi.on("before_agent_start", (_event, ctx) => {
if (projectTrustChecked) return;
projectTrustChecked = true;
if (ctx.isProjectTrusted() || process.argv.some((arg) => arg === "--no-approve" || arg === "-na")) return;
ctx.ui.notify(
\`Pi did not load project settings, extensions, skills, prompts, MCP servers or system prompt files for \${ctx.cwd} because the folder is not trusted. To load them, run the configured Pi binary there with the same PI_CODING_AGENT_DIR (if set), trust the folder or its parent with /trust, then start a new thread in that same worktree. To trust every project, add --approve to Pi's launch arguments in T3 Code settings; to keep them off without this notice, add --no-approve.\`,
"warning",
);
});

// Keep skill chips in the request. Pi expands only one leading /skill:
// command, so use its own frontmatter loader for every selected chip here.
pi.on("input", async (event, ctx) => {
Expand Down
Loading