fix(coding-agent): simplify session sharing links

This commit is contained in:
Armin Ronacher 2026-08-21 18:33:27 +02:00
parent 77f2d1235e
commit f4585b8bec
3 changed files with 34 additions and 47 deletions

View file

@ -6,6 +6,10 @@
- Added transcript usage notices for compaction and branch summaries when cache miss notices are enabled.
### Changed
- Changed session sharing to render clickable terminal links and Radius shares to display only the artifact's canonical URL.
### Fixed
- Fixed UTF-8 BOM markers preventing frontmatter and user configuration files from loading ([#8337](https://github.com/earendil-works/pi/issues/8337)).

View file

@ -502,7 +502,7 @@ export function expandTildePath(path: string): string {
const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/";
/** Get the share viewer URL for a gist ID or Radius artifact reference. */
/** Get the share viewer URL for a gist ID. */
export function getShareViewerUrl(gistId: string): string {
const baseUrl = process.env.PI_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;
return `${baseUrl}#${gistId}`;

View file

@ -6085,7 +6085,7 @@ export class InteractiveMode {
private async handleShareCommand(): Promise<void> {
// Radius artifacts natively support JSONL sessions. Gist fallback keeps the legacy HTML upload.
const jsonlFile = path.join(os.tmpdir(), "session.jsonl");
const htmlFile = path.join(os.tmpdir(), "session.html");
let htmlFile = null;
try {
try {
@ -6094,10 +6094,21 @@ export class InteractiveMode {
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
return;
}
const radiusResult = await this.tryShareViaRadius(jsonlFile);
if (radiusResult !== "radius-unavailable") return;
if (await this.tryShareViaRadius(jsonlFile)) return;
try {
const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
if (authResult.status !== 0) {
this.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
return;
}
} catch {
this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
return;
}
try {
htmlFile = path.join(os.tmpdir(), "session.html");
await this.session.exportToHtml(htmlFile, { themeName: theme.name });
} catch (error: unknown) {
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
@ -6107,7 +6118,9 @@ export class InteractiveMode {
} finally {
for (const tmpFile of [jsonlFile, htmlFile]) {
try {
fs.unlinkSync(tmpFile);
if (tmpFile !== null) {
fs.unlinkSync(tmpFile);
}
} catch {
// Ignore cleanup errors
}
@ -6115,16 +6128,16 @@ export class InteractiveMode {
}
}
private async tryShareViaRadius(tmpFile: string): Promise<"shared" | "radius-unavailable"> {
private async tryShareViaRadius(tmpFile: string): Promise<boolean> {
const provider = this.session.modelRuntime.getProvider("radius");
if (!provider) return "radius-unavailable";
if (!provider) return false;
const gatewayUrl = DEFAULT_RADIUS_GATEWAY;
const token = getAuthCredential(
await this.session.modelRuntime.getAuth("radius", { minOAuthValidityMs: 5 * 60_000 }),
);
if (!token) return "radius-unavailable";
if (!token) return false;
const loader = new BorderedLoader(this.ui, theme, "Uploading to Radius...");
this.editorContainer.clear();
@ -6151,30 +6164,22 @@ export class InteractiveMode {
body,
signal: loader.signal,
});
if (loader.signal.aborted) return "shared";
if (loader.signal.aborted) return true;
const json = (await response.json().catch(() => null)) as {
artifact?: { canonical_url?: string; url?: string; visibility?: string };
artifact?: { canonical_url: string };
error?: string;
} | null;
if (loader.signal.aborted) return "shared";
if (loader.signal.aborted) return true;
this.restoreShareEditor(loader);
if (!response.ok || !json?.artifact) {
this.showError(
`Failed to upload Radius artifact: ${json?.error || response.statusText || response.status}`,
);
return "shared";
return true;
}
const shareUrl = json.artifact.canonical_url || json.artifact.url;
if (!shareUrl) {
this.showError("Failed to upload Radius artifact: response did not include a share URL");
return "shared";
}
const artifactId = this.radiusArtifactIdFromUrl(shareUrl);
const previewUrl =
json.artifact.visibility === "public" && artifactId ? getShareViewerUrl(`radius/${artifactId}`) : shareUrl;
const visibilityNote = json.artifact.visibility === "public" ? "" : " (private; sign-in required)";
this.showStatus(`Share URL: ${previewUrl}${visibilityNote}\nRadius artifact: ${shareUrl}`);
return "shared";
const shareUrl = json.artifact.canonical_url;
this.showStatus(`Share URL: ${hyperlink(shareUrl, shareUrl)}`);
return true;
} catch (error: unknown) {
if (!loader.signal.aborted) {
this.restoreShareEditor(loader);
@ -6182,23 +6187,11 @@ export class InteractiveMode {
`Failed to upload Radius artifact: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
return "shared";
return true;
}
}
private async shareViaGist(tmpFile: string): Promise<void> {
// Check if gh is available and logged in
try {
const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
if (authResult.status !== 0) {
this.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
return;
}
} catch {
this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
return;
}
// Show cancellable loader, replacing the editor
const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
this.editorContainer.clear();
@ -6250,7 +6243,7 @@ export class InteractiveMode {
// Create the preview URL
const previewUrl = getShareViewerUrl(gistId);
this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
this.showStatus(`Share URL: ${hyperlink(previewUrl, previewUrl)}\nGist: ${hyperlink(gistUrl, gistUrl)}`);
} catch (error: unknown) {
if (!loader.signal.aborted) {
this.restoreShareEditor(loader);
@ -6266,16 +6259,6 @@ export class InteractiveMode {
this.ui.setFocus(this.editor);
}
private radiusArtifactIdFromUrl(value: string): string | null {
try {
const url = new URL(value);
const match = /^\/artifact\/([^/?#]+)\/?$/iu.exec(url.pathname);
return match ? match[1] : null;
} catch {
return null;
}
}
private async handleCopyCommand(options: { flashConfirmation?: boolean } = {}): Promise<void> {
const text = this.session.getLastAssistantText();
if (!text) {