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: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- Pi Extension Marketplace in Settings for browsing community catalog packages and safely installing, updating, or uninstalling user-scoped npm extensions through Pi's package manager.
- Complete Files CRUD with inline file/folder creation and rename, duplication, cut/copy/paste moves, system Trash deletion, file-manager reveal, active-file reveal, and secure worktree-relative Main-process operations.
- Public contribution, security, support, conduct, issue, and pull request guidance.
- Cross-platform development packaging for macOS arm64, Windows x64, and Linux x64.
- Projects, managed worktrees, file editing, Git changes, terminals, multi-tab pi Agent chat, settings, and support for user-installed standard Pi resources.

### Changed

- Made Installed the first and default Extensions tab, loading the remote Marketplace only when opened.
- Added safe `Remove from Pi` actions for user-configured local extension packages; this removes only their settings reference and never deletes local source files.
- Moved Projects and Settings controls into the Projects rail and redesigned Settings as an Orca-inspired full-window page with grouped navigation and search.
- Upgraded Git Changes into an Orca-inspired Source Control panel with filtering, collapsible conflict-aware groups, rich status metadata, safe stage/unstage/discard actions, and per-worktree commit drafts.
- Refined the workbench with quieter neutral chrome, a narrower Chat reading column, accessible icon tooltips, and a System appearance option that follows the operating system theme.

### Fixed

- Fixed `Remove from Pi` for relative local package sources by resolving the configured package path before asking Pi's PackageManager to remove the settings reference.
- Restored Marketplace extension installation for desktop-launched apps by locating the user's Node.js/npm runtime before invoking Pi's package manager.
- Widened Chat history and kept delete actions visible when session titles are too long for the popover row.

### Removed
Expand Down
29 changes: 29 additions & 0 deletions docs/pi-extension-marketplace/intent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Intent — Pi Extension Marketplace

## Problem

SpireCode can currently enumerate Pi extensions resolved from `~/.pi/agent/settings.json`, but users must leave the application and use the Pi CLI to discover, install, update, or remove packages. This makes the extension system hard to discover and encourages manual edits to Pi configuration.

Pi extensions execute with the same operating-system permissions as the Pi agent. A marketplace therefore crosses process, network, package-installation, persistence, and trust boundaries; it cannot be implemented as an embedded remote web page or a generic command runner.

## Outcome

Add an Extensions experience in Settings with two views:

- **Marketplace** browses community extension packages from Pi's package catalog.
- **Installed** shows user-configured resources and manages packages owned by Pi's package manager.

Users can install, update, and uninstall user-scoped npm packages through Pi's SDK after an explicit trust warning. The UI never labels catalog inclusion as official verification and never silently restarts an active agent session.

## Non-goals

- Project-scoped installation or mutation of `.pi/settings.json`.
- Git, local-path, private-registry, or arbitrary package sources.
- Ratings, reviews, favorites, automatic updates, or background installation.
- Security scanning or inferred fine-grained permissions.
- Loading `pi.dev` HTML or scripts in the Renderer.
- Treating package names, download counts, publishers, or catalog inclusion as verification.

## Risk classification

This is a **high-risk change** because it adds cross-process contracts, remote catalog ingestion, package code installation, and mutation of Pi's durable user settings. The user approved the proposed scope and security model before implementation.
49 changes: 49 additions & 0 deletions docs/pi-extension-marketplace/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Plan — Pi Extension Marketplace

## 1. Establish contracts with red-green tests

- Add catalog and managed-package types to Main and Renderer bindings.
- Extend IPC allowlists and argument validation with bounded query/page/sort and conservative npm source validation.
- Add tests proving unexpected fields, invalid names, arbitrary URLs/specifiers, stale sources, and project/Git sources are rejected; local removal tests must prove only exact configured user path sources are accepted.

## 2. Implement the catalog adapter

- Add `electron/domains/extensions/catalog.ts` with an injected fetch function.
- Construct only fixed-origin Pi Catalog URLs.
- Enforce timeout, manual redirects, response/content limits, and bounded cards.
- Parse the catalog's server-rendered `data-package-*` cards into validated plain objects.
- Cover realistic HTML fixtures, escaped content, pagination, malformed pages, unsafe links, redirects, oversized responses, and network failure.

## 3. Implement package management

- Add `electron/domains/extensions/service.ts` around an injected Pi `PackageManager` factory.
- Construct the production `DefaultPackageManager` with user agent directory and a trusted SettingsManager only for reading current user configuration; package mutations remain user-scoped.
- Map configured user npm sources and removable local settings references to resolved extension inventory without exposing resolved absolute managed install roots.
- Serialize install/update/remove and refresh authoritative extension state after success. Local removal delegates to PackageManager only for settings cleanup and must not delete source files.
- Add service tests with a fake PackageManager before production wiring.

## 4. Wire Main and generated bindings

- Add the extension service to `AppState`.
- Dispatch the four narrow commands in `electron/ipc.ts`.
- Add typed Renderer command wrappers and settings API methods.
- Keep the preload bridge unchanged because it already exposes only allowlisted command invocation.

## 5. Build the Settings experience

- Extract the Extensions section into focused components if needed.
- Add Marketplace/Installed tabs, search, sort, pagination, retry/empty states, package cards, authoritative install state, confirmation dialogs, mutation errors, and restart notice.
- Preserve installed resource information; show Update/Uninstall for npm packages, Remove from Pi for configured user local packages, and read-only behavior for direct/project resources.
- Add English and Simplified Chinese strings and responsive neutral-chrome styles.

## 6. Verify

- Run new domain, IPC, bindings, Settings UI, i18n, and style-contract tests.
- Run formatter, lint, typecheck, and full `pnpm check`.
- Review the final diff for arbitrary URL/command/path exposure, project-settings mutation, unsafe remote rendering, optimistic false success, and accidental official/verified wording.

## Acceptance evidence

- Deterministic tests show fixed-origin catalog ingestion and fail-closed package mutation.
- UI tests show trust confirmation, install single-flight, installed refresh, update/uninstall restrictions, failure recovery, and restart notice.
- `pnpm check` exits successfully.
85 changes: 85 additions & 0 deletions docs/pi-extension-marketplace/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Spec — Pi Extension Marketplace

## Product surface

`Settings → Extensions` contains `Marketplace` and `Installed` tabs.

### Marketplace

- Loads extension-only results from `https://pi.dev/packages`.
- Supports a trimmed text query, `downloads | recent | name` sorting, and numbered pagination.
- Displays package name, description, publisher, monthly downloads, relative publication time, package types, and trusted HTTPS links returned by Main.
- Labels entries as `Community` and explains that Pi Catalog inclusion is not verification.
- Represents loading, empty, unavailable, and retry states without affecting Installed.
- Shows `Install`, `Installing…`, or `Installed` from the authoritative installed-package result.

### Installed

- Preserves the existing resolved-extension inventory.
- Only entries backed by a configured user npm package are manageable.
- User-scoped npm packages expose Update and Uninstall. User-scoped local package sources expose Remove from Pi, which removes only the exact settings reference and never deletes local files. Direct extension files and project-scoped resources remain read-only.
- Mutations are single-flight in the Renderer and serialized in Main.
- Successful mutations refresh the installed inventory and show that agent sessions must be restarted before the change is fully active.

## Trust confirmation

Before installation, a modal identifies the package and publisher and states that extensions run with the Pi agent's OS permissions and may access project files, prompts, tool calls, credentials, environment variables, session data, network, and subprocesses. Installation requires an explicit confirmation action.

Uninstall requires confirmation. Updating does not remove data and may run after direct activation, but still shows the restart notice on success.

## Main-process boundary

### Catalog

- Renderer supplies only `query`, `sort`, and `page`.
- Main constructs the URL for the fixed `https://pi.dev/packages` origin with `type=extension`.
- Query is limited in UTF-8 size; page is a bounded positive integer; sort is an enum.
- Fetch uses a timeout, manual redirect policy, response-size limit, HTML content validation, and a bounded result count. At most one canonicalization redirect is followed, and only when it remains on `https://pi.dev/packages` with `type=extension`; all other redirects fail closed.
- Main parses server-rendered package cards and returns validated plain data. Remote HTML is never passed to Renderer.
- Only HTTPS npm, repository, and Pi Catalog links on expected hosts are returned.
- Catalog parser failure is a typed error and leaves Installed usable.

### Package mutations

- Main accepts only a validated npm package name and constructs `npm:<name>` itself for installation.
- Update accepts only the exact source of a currently configured **user-scoped npm package**.
- Uninstall accepts configured user npm packages. Remove from Pi accepts only exact, unfiltered user local package paths, resolves a relative configured source to PackageManager's installed-path identity in Main, and delegates only to PackageManager's settings removal; it never deletes the local source directory. Unknown, project, Git, filtered, or built-in sources are rejected.
- Main invokes Pi SDK `DefaultPackageManager`; it does not spawn a shell or implement npm installation itself.
- When Pi has no explicit `npmCommand`, Main locates a complete Node.js/npm runtime from the inherited `PATH` or bounded platform-specific user installation roots (including NVM, Volta, fnm, Homebrew, and standard Windows Node locations) and prepends only that runtime directory. An explicit Pi `npmCommand` remains authoritative.
- User scope is represented by SDK calls without `{ local: true }`.
- Mutations are serialized through one queue and authoritative state is reread afterward.

## IPC contracts

- `settings_extension_catalog_list(query, sort, page)`
- `settings_extension_package_install(packageName)`
- `settings_extension_package_update(source)`
- `settings_extension_package_remove(source)`

The existing `settings_extensions_list(worktreeId?)` result gains optional package source and management-kind metadata sufficient to distinguish npm management from removable local settings references, without exposing resolved absolute installation paths to Renderer.

## Package-name validation

Accept npm names matching unscoped `name` or scoped `@scope/name` syntax, using lowercase URL-safe npm identifier characters and a conservative length bound. Reject whitespace, control characters, versions/specifiers, paths, URLs, Git sources, flags, and empty segments.

## Lifecycle and concurrency

- Catalog reads may run concurrently with installed-list reads.
- Main package mutations run sequentially.
- A failed SDK operation does not alter Renderer state optimistically.
- A successful operation returns the refreshed installed inventory.
- SpireCode never hot-injects an extension into a running session and never restarts a session without user action.

## Accessibility and responsive behavior

- Tabs use tab semantics and keyboard-focusable buttons.
- Search, sort, pagination, links, and mutation buttons have accessible names.
- Confirmation dialogs trap focus through the existing Radix dialog primitive and expose title and description.
- Long package names and descriptions wrap or truncate without pushing actions outside the Settings content area.

## Failure behavior

- Offline, timeout, remote schema drift, invalid content, and HTTP failures produce a retryable catalog error.
- Invalid or stale package mutation requests fail closed with `INVALID_ARGUMENT` or `NOT_FOUND`.
- SDK failures surface their safe message and do not report success.
- A link rejected by Main is omitted rather than forwarded.
25 changes: 25 additions & 0 deletions electron/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import { KeyedLock } from "./core/keyedLock.js";
import { WindowCloseGuard } from "./windowCloseGuard.js";
import { DiagnosticsService } from "./domains/diagnostics/service.js";
import { resolveSpireResources } from "./domains/chat/spireSettings.js";
import {
DefaultPackageManager,
SettingsManager,
} from "@earendil-works/pi-coding-agent";
import { ExtensionCatalogService } from "./domains/extensions/catalog.js";
import { ExtensionPackageService } from "./domains/extensions/service.js";
import { ensureNpmOnPath } from "./domains/extensions/packageManagerEnvironment.js";

export interface SubscriptionEvent<T> {
subscriptionId: string;
Expand All @@ -40,6 +47,8 @@ export class AppState {
readonly watchers = new WatcherRegistry();
readonly windowCloseGuard = new WindowCloseGuard();
readonly diagnostics: DiagnosticsService;
readonly extensionCatalog: ExtensionCatalogService;
readonly extensionPackages: ExtensionPackageService;

private constructor(
readonly projects: ProjectService,
Expand All @@ -48,6 +57,22 @@ export class AppState {
dataDirectory: string,
) {
this.diagnostics = new DiagnosticsService(dataDirectory, "0.1.0");
this.extensionCatalog = new ExtensionCatalogService();
const agentDir = path.join(homedir(), ".pi", "agent");
const createPackageSettings = () =>
SettingsManager.create(homedir(), agentDir, { projectTrusted: false });
this.extensionPackages = new ExtensionPackageService(
() =>
new DefaultPackageManager({
cwd: homedir(),
agentDir,
settingsManager: createPackageSettings(),
}),
async () => {
if (!createPackageSettings().getNpmCommand()?.length)
await ensureNpmOnPath();
},
);
const root = (id: string) => projects.root(id);
const worktreeOperationLock = new KeyedLock();
this.filesystem = new FilesystemService(root, {
Expand Down
4 changes: 4 additions & 0 deletions electron/contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ describe("Electron host allowlists", () => {
"settings_language_get",
"settings_language_set",
"settings_agent_readiness",
"settings_extension_catalog_list",
"settings_extension_package_install",
"settings_extension_package_update",
"settings_extension_package_remove",
"settings_skills_list",
"settings_skill_set_enabled",
]),
Expand Down
4 changes: 4 additions & 0 deletions electron/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export const COMMANDS = [
"chat_session_abort",
"chat_session_delete",
"settings_extensions_list",
"settings_extension_catalog_list",
"settings_extension_package_install",
"settings_extension_package_update",
"settings_extension_package_remove",
"settings_skills_list",
"settings_skill_set_enabled",
"settings_language_get",
Expand Down
Loading
Loading