mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
parent
4b994d5e5e
commit
cea257439d
14 changed files with 164 additions and 5 deletions
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
### Added
|
||||
|
||||
- Added the `sessionShareDestination` setting to choose automatic, GitHub gist, or Radius uploads for `/share` ([#10870](https://github.com/earendil-works/pi/issues/10870))
|
||||
- Added `reachesTools` to tool definitions, so tools that reach other tools (such as `codemode` and `tool_search`) declare which tools a call may reach; the MCP extension uses it to wait for the servers a call needs and to decide whether a replacement `codemode` or `tool_search` tool can reach MCP tools
|
||||
- Added `aliases` to tool namespaces; codemode's `describeNamespace()` and the `namespace` option of `searchTools()` match them, and MCP namespaces use the server name
|
||||
- Added per-key OpenRouter model lists: with an OpenRouter key, Pi lists only the chat models that key may use, with the key's context size, max output, and prices, including guardrails and regional endpoints set as the base URL in `models.json`. The list is saved and used offline ([#10353](https://github.com/earendil-works/pi/issues/10353), [#10672](https://github.com/earendil-works/pi/pull/10672) by [@davidbrai](https://github.com/davidbrai))
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ Use `--session` when you already know the session path or ID. Use `--fork` to cr
|
|||
|
||||
## Export or share a session
|
||||
|
||||
Use `/export` to write the current session as HTML or JSONL. Use `/share` to upload it and get a viewer link. Pi uses a Radius artifact when Radius authentication is configured; otherwise, it uses a private GitHub gist.
|
||||
Use `/export` to write the current session as HTML or JSONL. Use `/share` to upload it and get a viewer link. By default, Pi uses a Radius artifact when Radius authentication is configured; otherwise, it uses a private GitHub gist. Set [`sessionShareDestination`](settings.md#sessions-and-context) to `gist` or `radius` to force one destination.
|
||||
|
||||
Review exported or shared sessions first. They can contain prompts, model responses, tool arguments, command output, file contents, and extension messages.
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ CLI tool options override this setting for one invocation. `--tools` with only `
|
|||
| Setting | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `sessionDir` | string | Agent session directory | Session storage directory. Relative paths resolve from the working directory. `PI_CODING_AGENT_SESSION_DIR` and `--session-dir` override this setting. |
|
||||
| `sessionShareDestination` | `"auto" \| "gist" \| "radius"` | `"auto"` | Destination used by `/share`. `auto` uses Radius when authenticated and otherwise creates a GitHub gist. `gist` always uses a gist. `radius` requires Radius authentication and never falls back to a gist. |
|
||||
|
||||
### Compaction
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ Use `!!` when you want to run a command without sending its output to the model.
|
|||
|
||||
Press `Ctrl+X` or run `/copy` to copy the last assistant response. Use `/export` to save the session as HTML or JSONL.
|
||||
|
||||
Use `/share` to upload the session and get a viewer link. With Radius authentication, the artifact is visible to your Radius organization. Otherwise, Pi creates a private GitHub gist through the GitHub CLI. Review the session first because it can contain prompts, tool output, file contents, and credentials exposed during the conversation.
|
||||
Use `/share` to upload the session and get a viewer link. By default, Radius authentication makes the artifact visible to your Radius organization; otherwise, Pi creates a private GitHub gist through the GitHub CLI. The [`sessionShareDestination`](settings.md#sessions-and-context) setting can force either destination. Review the session first because it can contain prompts, tool output, file contents, and credentials exposed during the conversation.
|
||||
|
||||
## Adjust the terminal
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,24 @@
|
|||
],
|
||||
"default": "auto"
|
||||
},
|
||||
"sessionShareDestination": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"const": "auto"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "gist"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "radius"
|
||||
}
|
||||
],
|
||||
"description": "Destination used by /share. \"auto\" uses Radius when authenticated and otherwise creates a GitHub gist.",
|
||||
"default": "auto"
|
||||
},
|
||||
"steeringMode": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { DEFAULT_MAX_AGENT_RETRY_DELAY_MS } from "@earendil-works/pi-ai/utils/re
|
|||
/** Runtime defaults shared by settings accessors and the published JSON Schema. */
|
||||
export const SETTINGS_DEFAULTS = {
|
||||
transport: "auto",
|
||||
sessionShareDestination: "auto",
|
||||
steeringMode: "one-at-a-time",
|
||||
followUpMode: "one-at-a-time",
|
||||
compaction: {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type {
|
|||
MermaidRenderingMode,
|
||||
PackageSource,
|
||||
QuietStartup,
|
||||
SessionShareDestination,
|
||||
Settings,
|
||||
ThinkingBudgetsSettings,
|
||||
TransportSetting,
|
||||
|
|
@ -41,6 +42,7 @@ export type {
|
|||
ProviderRetrySettings,
|
||||
QuietStartup,
|
||||
RetrySettings,
|
||||
SessionShareDestination,
|
||||
Settings,
|
||||
TerminalSettings,
|
||||
ThinkingBudgetsSettings,
|
||||
|
|
@ -802,6 +804,19 @@ export class SettingsManager {
|
|||
this.save();
|
||||
}
|
||||
|
||||
getSessionShareDestination(): SessionShareDestination {
|
||||
const destination = this.settings.sessionShareDestination;
|
||||
return destination === "gist" || destination === "radius"
|
||||
? destination
|
||||
: SETTINGS_DEFAULTS.sessionShareDestination;
|
||||
}
|
||||
|
||||
setSessionShareDestination(destination: SessionShareDestination): void {
|
||||
this.globalSettings.sessionShareDestination = destination;
|
||||
this.markModified("sessionShareDestination");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getCompactionEnabled(): boolean {
|
||||
return this.settings.compaction?.enabled ?? SETTINGS_DEFAULTS.compaction.enabled;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,13 @@ export const SettingsSchema = Type.Object(
|
|||
{ default: SETTINGS_DEFAULTS.transport },
|
||||
),
|
||||
),
|
||||
sessionShareDestination: Type.Optional(
|
||||
Type.Union([Type.Literal("auto"), Type.Literal("gist"), Type.Literal("radius")], {
|
||||
description:
|
||||
'Destination used by /share. "auto" uses Radius when authenticated and otherwise creates a GitHub gist.',
|
||||
default: SETTINGS_DEFAULTS.sessionShareDestination,
|
||||
}),
|
||||
),
|
||||
steeringMode: Type.Optional(
|
||||
Type.Union([Type.Literal("all"), Type.Literal("one-at-a-time")], {
|
||||
default: SETTINGS_DEFAULTS.steeringMode,
|
||||
|
|
@ -464,6 +471,7 @@ export interface CodemodeSettings extends Static<typeof CodemodeSettingsSchema>
|
|||
export type DefaultProjectTrust = NonNullable<SettingsInput["defaultProjectTrust"]>;
|
||||
export type QuietStartup = NonNullable<SettingsInput["quietStartup"]>;
|
||||
export type TransportSetting = NonNullable<SettingsInput["transport"]>;
|
||||
export type SessionShareDestination = NonNullable<SettingsInput["sessionShareDestination"]>;
|
||||
export type PackageSource = Static<typeof PackageSourceSchema>;
|
||||
export type FullscreenExitOutput = NonNullable<SettingsInput["fullscreenExitOutput"]>;
|
||||
export type TuiMode = NonNullable<SettingsInput["tuiMode"]>;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|||
{ name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" },
|
||||
{ name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" },
|
||||
{ name: "import", description: "Import and resume a session from a JSONL file" },
|
||||
{ name: "share", description: "Share session as a secret GitHub gist" },
|
||||
{ name: "share", description: "Share session via a Radius artifact or a GitHub gist" },
|
||||
{ name: "bug", description: "Report a bug to the Pi developers", argumentHint: "<description>" },
|
||||
{ name: "copy", description: "Copy last agent message to clipboard" },
|
||||
{ name: "name", description: "Set session display name" },
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
type FullscreenExitOutput,
|
||||
type MermaidRenderingMode,
|
||||
type QuietStartup,
|
||||
type SessionShareDestination,
|
||||
type TuiMode,
|
||||
type WarningSettings,
|
||||
} from "../../../core/settings-manager.ts";
|
||||
|
|
@ -70,6 +71,7 @@ export interface SettingsConfig {
|
|||
steeringMode: "all" | "one-at-a-time";
|
||||
followUpMode: "all" | "one-at-a-time";
|
||||
transport: Transport;
|
||||
sessionShareDestination: SessionShareDestination;
|
||||
httpIdleTimeoutMs: number;
|
||||
cacheWarmingMode: CacheWarmingMode;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
|
|
@ -111,6 +113,7 @@ export interface SettingsCallbacks {
|
|||
onSteeringModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||
onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||
onTransportChange: (transport: Transport) => void;
|
||||
onSessionShareDestinationChange: (destination: SessionShareDestination) => void;
|
||||
onHttpIdleTimeoutMsChange: (timeoutMs: number) => void;
|
||||
onCacheWarmingModeChange: (mode: CacheWarmingMode) => void;
|
||||
onModelThinkingLevelChange: (provider: string, modelId: string, level: ThinkingLevel) => void;
|
||||
|
|
@ -509,6 +512,14 @@ export class SettingsSelectorComponent extends Container {
|
|||
currentValue: config.transport,
|
||||
values: ["sse", "websocket", "websocket-cached", "auto"],
|
||||
},
|
||||
{
|
||||
id: "share-destination",
|
||||
label: "Session share destination",
|
||||
description:
|
||||
"Where to upload sessions with /share (auto prefers Radius when authenticated, otherwise GitHub Gist)",
|
||||
currentValue: config.sessionShareDestination,
|
||||
values: ["auto", "gist", "radius"],
|
||||
},
|
||||
{
|
||||
id: "http-idle-timeout",
|
||||
label: "HTTP idle timeout",
|
||||
|
|
@ -900,6 +911,9 @@ export class SettingsSelectorComponent extends Container {
|
|||
case "transport":
|
||||
callbacks.onTransportChange(newValue as Transport);
|
||||
break;
|
||||
case "share-destination":
|
||||
callbacks.onSessionShareDestinationChange(newValue as SessionShareDestination);
|
||||
break;
|
||||
case "http-idle-timeout": {
|
||||
const choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.label === newValue);
|
||||
if (choice) {
|
||||
|
|
|
|||
|
|
@ -4938,6 +4938,7 @@ export class InteractiveMode {
|
|||
steeringMode: this.session.steeringMode,
|
||||
followUpMode: this.session.followUpMode,
|
||||
transport: this.settingsManager.getTransport(),
|
||||
sessionShareDestination: this.settingsManager.getSessionShareDestination(),
|
||||
httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
|
||||
cacheWarmingMode: this.settingsManager.getCacheWarmingMode(),
|
||||
thinkingLevel: this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL,
|
||||
|
|
@ -5009,6 +5010,9 @@ export class InteractiveMode {
|
|||
this.settingsManager.setTransport(transport);
|
||||
this.session.agent.transport = transport;
|
||||
},
|
||||
onSessionShareDestinationChange: (destination) => {
|
||||
this.settingsManager.setSessionShareDestination(destination);
|
||||
},
|
||||
onHttpIdleTimeoutMsChange: (timeoutMs) => {
|
||||
this.settingsManager.setHttpIdleTimeoutMs(timeoutMs);
|
||||
configureHttpDispatcher(timeoutMs);
|
||||
|
|
|
|||
|
|
@ -66,7 +66,14 @@ export async function shareSession(context: SessionShareContext): Promise<void>
|
|||
context.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
return;
|
||||
}
|
||||
const destination = context.session.settingsManager.getSessionShareDestination();
|
||||
if (destination !== "gist") {
|
||||
if (await tryShareViaRadius(jsonlFile, context)) return;
|
||||
if (destination === "radius") {
|
||||
context.showError("Radius authentication is not configured. Run '/login radius' first.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { EventEmitter } from "node:events";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const childProcessMocks = vi.hoisted(() => ({
|
||||
spawn: vi.fn(),
|
||||
|
|
@ -23,6 +23,7 @@ function deferred(): { promise: Promise<void>; resolve: () => void } {
|
|||
|
||||
describe("shareSession", () => {
|
||||
beforeAll(() => initTheme("dark"));
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("keeps concurrent session exports isolated", async () => {
|
||||
const uploads: string[] = [];
|
||||
|
|
@ -53,6 +54,7 @@ describe("shareSession", () => {
|
|||
getBranch: () => [],
|
||||
},
|
||||
state: { systemPrompt: name, tools: [] },
|
||||
settingsManager: { getSessionShareDestination: () => "auto" },
|
||||
modelRuntime: { getProvider: () => undefined },
|
||||
exportToHtml: async (filePath: string) => {
|
||||
writeFileSync(filePath, name);
|
||||
|
|
@ -85,4 +87,70 @@ describe("shareSession", () => {
|
|||
expect(uploads).toEqual(["A", "B"]);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
// #10870: users need persistent gist shares even when Radius authentication is configured.
|
||||
it("forces gist sharing without checking Radius authentication", async () => {
|
||||
const getProvider = vi.fn(() => ({ id: "radius" }));
|
||||
childProcessMocks.spawn.mockImplementation((_command, args: string[]) => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
kill: vi.fn(),
|
||||
});
|
||||
queueMicrotask(() => {
|
||||
child.stdout.end("https://gist.github.com/test/forced-gist\n");
|
||||
child.stderr.end();
|
||||
child.emit("close", 0);
|
||||
});
|
||||
expect(readFileSync(args.at(-1)!, "utf8")).toBe("html");
|
||||
return child;
|
||||
});
|
||||
const statuses: string[] = [];
|
||||
|
||||
await shareSession({
|
||||
session: {
|
||||
sessionManager: { getSessionId: () => "gist", getCwd: () => "/tmp", getBranch: () => [] },
|
||||
state: { systemPrompt: "", tools: [] },
|
||||
settingsManager: { getSessionShareDestination: () => "gist" },
|
||||
modelRuntime: { getProvider },
|
||||
exportToHtml: async (filePath: string) => writeFileSync(filePath, "html"),
|
||||
},
|
||||
ui: { setFocus() {}, requestRender() {} },
|
||||
editorContainer: { clear() {}, addChild() {} },
|
||||
editor: {},
|
||||
showStatus: (message: string) => statuses.push(message),
|
||||
showError: vi.fn(),
|
||||
} as never);
|
||||
|
||||
expect(getProvider).not.toHaveBeenCalled();
|
||||
expect(childProcessMocks.spawn).toHaveBeenCalledWith("gh", [
|
||||
"gist",
|
||||
"create",
|
||||
"--public=false",
|
||||
expect.any(String),
|
||||
]);
|
||||
expect(statuses[0]).toContain("https://pi.dev/session/#forced-gist");
|
||||
});
|
||||
|
||||
it("reports missing Radius authentication when Radius is forced", async () => {
|
||||
const errors: string[] = [];
|
||||
|
||||
await shareSession({
|
||||
session: {
|
||||
sessionManager: { getSessionId: () => "radius", getCwd: () => "/tmp", getBranch: () => [] },
|
||||
state: { systemPrompt: "", tools: [] },
|
||||
settingsManager: { getSessionShareDestination: () => "radius" },
|
||||
modelRuntime: { getProvider: () => undefined },
|
||||
},
|
||||
ui: { setFocus() {}, requestRender() {} },
|
||||
editorContainer: { clear() {}, addChild() {} },
|
||||
editor: {},
|
||||
showStatus: vi.fn(),
|
||||
showError: (message: string) => errors.push(message),
|
||||
} as never);
|
||||
|
||||
expect(errors).toEqual(["Radius authentication is not configured. Run '/login radius' first."]);
|
||||
expect(childProcessMocks.spawnSync).not.toHaveBeenCalled();
|
||||
expect(childProcessMocks.spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,6 +63,28 @@ describe("SettingsSelectorComponent", () => {
|
|||
expect(onWheelScrollLinesChange.mock.calls.flat()).toEqual([10, "auto", 1]);
|
||||
});
|
||||
|
||||
it("cycles through share destinations", () => {
|
||||
const onSessionShareDestinationChange = vi.fn();
|
||||
const config = {
|
||||
sessionShareDestination: "auto",
|
||||
warnings: {},
|
||||
defaultModel: "not set",
|
||||
availableDefaultModels: [],
|
||||
availableThinkingLevels: [],
|
||||
modelThinkingLevels: {},
|
||||
availableThemes: [],
|
||||
} as unknown as SettingsConfig;
|
||||
const callbacks = {
|
||||
onSessionShareDestinationChange,
|
||||
} as unknown as SettingsCallbacks;
|
||||
const list = new SettingsSelectorComponent(config, callbacks).getSettingsList();
|
||||
list.selectItem("share-destination");
|
||||
|
||||
for (let i = 0; i < 3; i++) list.handleInput("\r");
|
||||
|
||||
expect(onSessionShareDestinationChange.mock.calls.flat()).toEqual(["gist", "radius", "auto"]);
|
||||
});
|
||||
|
||||
it("keeps the configured fixed theme marked while browsing", () => {
|
||||
const config = {
|
||||
defaultModel: "not set",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue