Skip to content

Latest commit

 

History

History
537 lines (450 loc) · 28.9 KB

plugins.md

File metadata and controls

537 lines (450 loc) · 28.9 KB

Plugin packages

chartr installs packages; it does not build projects. Installation has no scripts, hooks, package manager, compiler, or Rust-toolchain dependency. Packages are user-selected code, with no marketplace or audit requirement. Install sources you trust; manifest permissions describe the host APIs a plugin can use, not a guarantee that its overall behavior is confined to those APIs.

Every package is a directory with chartr-plugin.toml at its root. A web package includes the declared HTML entry and its assets. Entries may live in subdirectories; asset URLs resolve within the package root, including filenames that require URL encoding.

Every manifest also declares one free Hugeicons Stroke Rounded export by its canonical name, for example icon = "Clock01Icon". The matching SVG must be packaged at icons/Clock01Icon.svg. chartr validates that file before install or load and uses it in sidebar, outer, and pane-local tabs. Installation and discovery share the same package validator, including for disabled plugins. Declared assets must be regular files inside the package; absolute paths, parent traversal, and symlinks escaping the package are rejected. Installation rejects source symlinks and omits .git and target directories.

An embedded package supplies a prebuilt native library using the versioned surface ABI. It executes trusted native code with the user's authority, while Chartr retains ownership of GPUI objects. See native plugins. Legacy kind = "hosted" packages are no longer supported.

Every installed plugin must be prebuilt. Installation never invokes a compiler, build script, dependency installer, or package hook. Web plugins ship ready-to-run HTML, JavaScript and assets. Native plugins ship platform binaries; missing builds produce an error, never a local compilation fallback.

Installation sources

  • Local folder: chartr copies the selected package into private staging before showing the confirmation.
  • Git repository: chartr shallow-clones the default branch. Embedded plugins may declare an HTTPS release directory; Chartr then fetches the platform archive and verifies its SHA-256 checksum. Cloning has a two-minute deadline and disables interactive terminal credential prompts. Cancel in Settings, or close Settings, to stop preparation and its Git process group.

After the user confirms the package details and declared permissions, chartr validates the staged contents again and queues the package under $XDG_DATA_HOME/chartr/plugin-pending/<plugin id>/. Later leaves the current catalog and package assets unchanged, including after disable/re-enable. At the next startup, before loading any plugins, chartr atomically activates the queued package at plugins/<plugin id>/. Activation failure preserves the installed copy and retains the pending package for retry; interruption after activation can safely reapply the same package. Persistent plugin data lives outside both directories and survives replacement. Restart performs that startup immediately.

Each new installation records its source and, for Git, the checked-out commit in .chartr-install.json inside the managed package. Settings shows this alongside the manifest version. Activation preserves the original record; package-supplied records are overwritten on installation. Older or manually copied packages may have no record. This record is for display, not verification of the author or package contents. Updating means installing the source again; there is no automatic update or version-history service.

Each plugin has exactly one entry in Settings: its name and info, gear, and enable controls on the first line, then a short description that wraps across the full width. Prerequisite errors appear below the description. The host owns this layout; plugins cannot add extra rows. The gear opens the plugin's single configuration page, combining native settings controls with prerequisite setup links and host permissions. The info icon opens Plugin Information, which contains the package details, access summary, and Uninstall button. Build-time native plugins may contribute a GPUI view. Web and embedded plugins declare [settings] fields; chartr renders these with the same native controls as its own forms. No plugin HTML or JavaScript runs inside the Settings window. Legacy settings_entry manifests are rejected with a migration message; replace that declaration with the schema below. Existing plugin data is retained.

Uninstall confirms removal, closes the plugin's panes and configuration view, and removes its installed and bundled packages and any pending update. Source repositories, plugin data, and preferences are kept. Bundled removals are saved as uninstalled = true in that plugin's settings so they stay removed on restart. To restore a compiled-in bundle, clear that marker and restart; it remains disabled until enabled. Reinstalling an embedded or web package also leaves it disabled. If removal fails, Settings reports the error and leaves the plugin disabled for retry. System packages can be disabled here; remove them with the system package manager. A newly queued plugin appears in the catalog after restart.

Disabling or uninstalling a prerequisite first prompts with the complete list of dependent plugins, including indirect dependents. Confirmation disables that entire cascade, closes its panes, and persists the disabled state; cancellation changes nothing. Dependents remain installed and are never enabled implicitly when their provider returns. Missing, disabled, or cyclic prerequisites block enabling, including during startup. These rules apply to all plugin tiers.

Web packages are architecture-independent. Embedded native packages must publish binaries for each supported OS and architecture. Native libraries load only when a pane is opened; browsing the plugin catalog never executes plugin code.

Build-time GPUI modules remain part of the application build. Independently built GPUI libraries are still rejected: embedded plugins exchange plain data and opaque native handles instead of Rust GUI objects or GPUI runtime state.

The bundled Agent plugin is one such native module. Its GPUI pane receives the owning space's display context and a host capability that opens a normal chartr-owned terminal. Agent commands, arguments, environment, and prompt delivery remain structured until the native plugin quotes each shell word; the resulting session belongs to the pane's space and follows the same persistence and close lifecycle as a terminal opened by the user.

Background status

Bundled native plugins can implement Plugin::background_status(&App) to return a BackgroundStatus with a short label, tooltip detail, and Idle, Running, or Error state. This query must be fast and perform no blocking I/O. The workspace checks for changes once per second, independently of pane visibility. Clicking a status opens the plugin’s settings; disabling the plugin removes its status. The bar can be hidden without stopping any service, and its visibility is saved as [general] show_status_bar in settings.toml.

Manifest

A complete web example (optional fields shown with their defaults):

manifest_version = 2
id = "com.example.notes"
name = "Notes"
description = "Keep notes alongside your work."
version = "0.1.0"
kind = "web"
icon = "NoteIcon"             # package includes icons/NoteIcon.svg
entry = "index.html"

[capabilities]
multiplicity = "per_space"    # or "multiple"
cloneable = false
restorable = false
session_binding = false

[permissions]
project_files = "none"        # or "read", "read_write"
network = []                  # e.g. ["api.example.com", "*.example.org"]
process = false
session = false
wayfinder = false            # map workflow and registered-agent launching

Portable plugins can add native settings without a settings document:

[settings]
file = "settings.json"         # relative to this plugin's private data directory

[[settings.fields]]
key = "format"                 # top-level JSON property
label = "Clock format"
type = "select"
default = "24"
options = [
    { value = "24", label = "24-hour" },
    { value = "12", label = "12-hour" },
]

[[settings.fields]]
key = "show_seconds"
label = "Show seconds"
description = "Include seconds in the display."  # optional
type = "toggle"
default = true

select stores a string from its declared options; toggle stores a boolean. Field keys and option values must be unique, and a select's default must be one of its options. The host accepts up to 64 fields and 64 options per select. Unknown control types are rejected. Settings paths must remain inside private plugin data; parent directories must already exist.

The file is a JSON object, limited to 1 MiB. Missing fields use their declared defaults. Opening Settings does not write a file. Each change re-reads the file, updates the selected key, preserves other keys, and replaces the file atomically. Unreadable, malformed, or unsupported saved values produce a native error and are not overwritten; correct the file and use Reload. Plugins read the same file through data.read (the optional examples/plugins/clock example checks it on each tick).

The ID is the package identity used for replacement, data, preferences, and saved panes. Use a stable reverse-DNS name. IDs accept ASCII letters, digits, ., -, and _, up to 128 characters, and cannot start with .. Installed directory names must match the ID. version is a display string; chartr does not compare versions or prevent downgrades. This build accepts only manifest version 2; unknown TOML fields are ignored.

Web and embedded packages contribute one main pane. per_space focuses an existing matching pane when opened again; multiple permits additional panes. cloneable enables opening another instance, and restorable enables reopening saved panes. Web cloning and restoration create a fresh document; there is no host API for serializing per-pane web state. session_binding lets a pane bind to the terminal from which its launcher was opened (required for this capability); permissions.session separately grants access to that binding. Native settings schemas grant no project, network, process, or terminal access.

The data.* API always accesses $XDG_DATA_HOME/chartr/plugin-data/<plugin id>/. This storage is shared across the plugin's panes, spaces, and native settings form, not allocated per instance. File APIs do not create parent directories.

The project-file and network grants constrain their respective APIs. process = true grants execution with the user's account authority, including filesystem and network access outside those brokers. session = true allows terminal input, which can execute commands with the bound terminal's authority. Neither is a sandboxed form of execution. The explicit unsafe-filesystem setting overrides project-file API restrictions; it does not change the private data root. wayfinder = true grants map and source reads, ticket-claim updates, and launches through registered agents with the user's account authority. It does not grant arbitrary project writes, raw terminal input, process execution, or unrestricted access to native services. It is still an execution capability: a registered agent can carry out the workflow's prompt with its normal permissions.

Plugin prerequisites and native services

A plugin declares required providers with a short explanation of each prerequisite:

[[dependencies]]
plugin = "com.chartr.agent"
feature = "Agent launching"

[[dependencies]]
plugin = "com.chartr.skills"
feature = "Ticket methods"

Settings displays the provider and feature; missing providers are never automatically installed or enabled. Consumers check configuration as well as availability and explain how to finish setup. The host enforces these prerequisites and activates providers first at startup; it does not resolve package versions.

Trusted native plugins can return ServiceExport::new(service) from their optional Plugin::services method. InstanceContext.services.get::<T>(plugin_id) resolves a live export in the owning catalog. Disablement removes its entries; reenablement publishes fresh exports. Resolve services again before acting. Native code is already trusted; these are shared typed contracts, not a sandbox.

The contracts include services::Agents (registered names and input preparation), ConversationAgents (registered profiles for the Inbox launcher), services::Skills (asynchronous enabled-source scanning, method text, directories and commit provenance), Prompts, and PromptTemplates. Agent and Skills own their registries. Each plugin has one configuration surface opened through its gear in Settings → Plugins. Pane setup shortcuts use InstanceContext.plugin_settings to open that surface instead of rendering another configuration page in the workspace.

Native settings return Option<chartr_plugin::SettingsView>, created with SettingsView::new(entity, cx). The entity implements RenderSettings, whose return type is the shared SettingsPage; a pane's arbitrary AnyView is no longer a settings contribution. The host adapter retains the entity, observes its notifications, and routes actions back to it.

Use SettingsPage::flow for forms in the host scroll, fill for pages with independently scrolling lists, and scroll for management pages that scroll as a whole. The shell owns section spacing, exposes no Styled implementation, and paints no background. The Settings window owns the background and outer inset. Do not wrap embedded settings in another padded or painted page.

With the SDK's ui feature, chartr_plugin::ui supplies semantic label, heading, caption, action, icon_action, PageHeader, Notice, ModalOverlay, DialogSurface, forms, pane foundations, cards, template chips, and table surface helpers. Actions expose primary() and destructive(); typography and action dimensions are selected by their semantic components. Dialog content and behavior remain plugin-owned. Native popup hosting stays with the host so dialogs continue to appear above embedded webviews. The host's existing form and font exports delegate to these same definitions.

DialogSurface owns the outline and clipping; compose its contents with dialog_header, dialog_body, and optional dialog_actions. Include a footer only when the dialog needs actions such as Save, Cancel, or Delete. Read-only previews can use just a header with a close control and a body. These sections own their compact padding, so header and footer dividers reach the surface edges. Dialog titles use normal label typography. Keep overflowing content in the body so the header and actions remain visible.

The core SDK remains usable without ui (see the Hello example). A module that builds native UI opts into it in its dependency:

chartr-plugin = { path = "path/to/chartr-plugin", features = ["ui"] }

The workspace test suite scans every plugin Rust source directory, including new plugins, for copied background/border/radius/shadow recipes and direct raw label/button construction. Extend the shared components for new presentation requirements; do not add local styling exceptions. Compile-fail tests protect the shell/action API, while rendered catalog tests check actual native and declarative settings after theme and UI-scale changes. Portable plugins continue to contribute host-rendered settings schemas, not HTML settings pages.

Provider identity is defined once in chartr-agent: the declaration generates the enum, canonical integration IDs, aliases, and explicit input capabilities. Launchers, conversation state, and Herdr process recognition consume it. Preserve provider-specific protocol parsing in its adapter; do not create another list of provider names to infer shared capabilities.

Native stores use chartr-storage::write_atomic for private configuration and StagedWrite for validated project writes. Both sync the staged file before publishing it and retain existing permissions. Callers retain format validation, conflict checks, and authorization of project paths. Use create_new() when replacement is forbidden; it must not clobber a concurrently created file.

The bundled Saved Prompts plugin exports services::Prompts from com.chartr.prompts. It owns a shared saved-prompt library managed in its plugin settings page. list(cx) returns records with stable IDs, display titles, and prompt text; resolve(id, cx) reads a current record by ID. Consumers inject only the prompt field. The title is never part of the injection payload. See Saved Prompts for persistence and settings behavior.

InstanceContext.terminal.prepare(cx) returns a task resolving to an attached PreparedTerminal with a real session id; send accepts a TerminalLaunch containing a validated POSIX shell command and separate optional typed input. All commands run from owner-only temporary scripts in a dedicated shell. The interactive shell is replaced before typed input is delivered, and the terminal session ends when the agent exits or fails. Ordinary session.send remains raw input. Successful queueing does not establish that the agent started or stayed alive. This lets a consumer claim a ticket before delivering its launch command. terminal.focus(id, window, cx) selects that session in its owning space. These Rust capabilities stay host-side. The narrow Wayfinder web API below uses them without exposing arbitrary service calls or shell input to JavaScript.

Web appearance

The host sets opt-in --chartr-* CSS custom properties on the document root and refreshes them when Chartr's theme or UI scale changes. Web panes retain their own stylesheet; consuming the tokens lets them match native controls.

  • Typography: --chartr-ui-font, --chartr-mono-font, --chartr-font-size, --chartr-font-small, and --chartr-font-large.
  • Sizing: --chartr-space-1 through --chartr-space-6, --chartr-control-height, --chartr-icon-size, --chartr-icon-button-size, and --chartr-radius. All dimensions are CSS pixels resolved at the current UI scale.
  • Colors: --chartr-bg, --chartr-panel, --chartr-surface, --chartr-text, --chartr-muted, --chartr-border, --chartr-border-variant, --chartr-control, --chartr-hover, --chartr-selected, --chartr-accent, --chartr-focus, --chartr-warning, --chartr-error, --chartr-success, --chartr-scrollbar, and --chartr-scrollbar-hover. --chartr-color-scheme is light or dark.

For canvas rendering, read computed styles at startup and listen for the chartr:theme window event to refresh colors and font metrics. Its detail contains the current token values.

Web host operations

window.chartr.invoke(action, options) runs host operations through a bounded, ordered background queue for each pane. A full queue rejects new requests instead of blocking the UI. Closing the pane suppresses replies and queued work; an already-running operation may finish in the background. Replies are scoped to the document that issued each request, so navigation cannot deliver an old reply to a new page. Invalid calls reject their own promise. Navigation clears pending promises; a ten-minute reply deadline also covers time spent waiting in the ordered queue.

invoke returns a Promise for the result below and rejects with an Error on failure. All path, data, command, argument, and URL values are strings; optional fields are marked ?.

Action Options Result Required grant
project.read { path } UTF-8 string project_files = "read" or "read_write"
project.write { path, data } true project_files = "read_write"
data.read { path } UTF-8 string Always available
data.write { path, data } true Always available
network.fetch { url } { status: number, body: string } Host in network
process.run { command, args?: string[], cwd? } { status: number | null, stdout: string, stderr: string } process = true
session.metadata Omit options { id: string, workspace: string, title: string, agent: string | null, cwd: string | null } session = true and a live binding
session.send { data } true session = true and a live binding
const text = await window.chartr.invoke("project.read", { path: "README.md" });
await window.chartr.invoke("data.write", { path: "notes.txt", data: text });

network.fetch performs an HTTP(S) GET without custom headers or a request body; HTTP 4xx/5xx responses reject. Allowlist entries match hostnames, not paths or ports. A URL entry contributes its hostname; *.example.org includes the base domain and its subdomains. Packaged web documents cannot make direct network connections under chartr's content security policy; use the host API.

process.run launches a command directly, without a shell unless the plugin explicitly invokes one. Arguments default to an empty array. A nonzero exit code is returned as status, not a rejection; termination by signal returns null. Output is decoded as UTF-8 with replacement for invalid bytes. session.send writes the supplied terminal input as-is; it does not add a newline.

Requests are limited to 1 MiB of encoded JSON. File reads and HTTP response bodies are limited to 8 MiB; process stdout and stderr share an 8 MiB limit. HTTP fetches (including all redirects) and processes have a 30-second execution deadline. Timeouts, permission failures, and exceeded limits reject the returned promise. Processes that time out or exceed the output limit are terminated along with their process group.

Each HTTP redirect must pass the manifest's network-host allowlist, just like the initial URL. At most ten redirects are followed. Safe filesystem operations open regular files beneath the project root or the plugin's shared data root; dangling links and symlinks swapped into a validated path are rejected. Existing links to files inside the allowed root continue to work. Explicit unsafe filesystem permission still bypasses the project-root restriction, but not the private data-root check.

project.write and data.write require both path and a string data payload. An explicitly empty string empties the file; an omitted or mistyped payload is rejected before opening it. Whole-file writes atomically replace the destination, preserving existing permission bits. Readers see a complete old or new file; concurrent writes use last-completed-write semantics. New files are private to the user. These operations write regular files, rather than devices or pipes.

process.run accepts { command, args?, cwd? }. By default it runs in the owning project, or the plugin data directory for folderless/settings views. A relative cwd resolves from that same base; an absolute cwd is also accepted. Process execution remains subject to the manifest's process permission.

Bound-session metadata reflects current session information. Terminal reattachment retains the capability and redirects input to the new attachment; closing the session makes its capabilities unavailable.

Wayfinder workflow

These actions require permissions.wayfinder = true and an owning space pane; they are unavailable to native settings schemas. No identity-based exemption is used: an installed web package must declare the same grant as bundled Wayfinder. All actions use the existing ordered queue and document-scoped replies.

Action Options Result
wayfinder.snapshot Omit Space/folder, discovered maps/tickets with safe rendered Markdown, agent names, qualified skill names and setup diagnostics
wayfinder.preview { slug: string, ticket: number, method?: string, note?: string } { preview: number, text: string, sources: string[] }
wayfinder.launch { preview: number, agent: string } { session: string }
wayfinder.release { slug: string, ticket: number, session: string } true
wayfinder.focus { slug: string, ticket: number } true
wayfinder.open { slug: string, ticket?: number, target?: string } true
wayfinder.settings { provider: "agent" | "skills" } true

Preview requires an existing map slug and a ready ticket. The optional method pins an exact source/skill; omission selects by ticket type. Notes are bounded to 16 KiB and the composed prompt to 192 KiB. A preview ID is one-shot, document-bound and replaced by the next preview. Launch rechecks the source content and on-disk map, prepares a real terminal, writes its claim, then delivers adapter-correct input. Closing or navigating before delivery cancels the launch and releases its own claim. An already delivered launch is not terminated.

Release must supply the current claim's session ID; it never stops the terminal. The UI asks for a second confirmation. Open without a target opens the map/ticket file; relative targets resolve beside that file and must remain inside the space. HTTP(S) targets open through the OS. Raw HTML is escaped, images render as links, and no project Markdown executes in the web document.

Android companion

Companion is excluded from current desktop builds, including its listener dependency and host bridge. Its source and the wire protocol are retained for future development of the Android app in ../chartr-mobile.

Prompt template providers

PromptTemplates is the shared read contract consumed by Markdown Prompt and available to future native consumers. Saved Prompts, Skills and Wayfinder each own their contributions. Providers never choose the consumer's destination or write files on its behalf.

A template has a stable provider-local id, a user-facing title, and a literal Markdown prompt body (the existing SavedPrompt value type). Identity is the pair (plugin_id, template_id), never the title. Identical titles from different providers coexist; there is no name-based precedence or fallback. Provider order is lexicographic plugin ID; each provider controls its own template order. Renaming a title preserves references. Removing a provider or ID makes a referenced template unavailable and prevents Apply. Bodies are not recursively expanded, interpreted as expressions, or executed. Consumers add no headings or other formatting to a body.

Dynamic providers call PromptTemplates::changed(cx) after committing a content change. Consumers observe PromptTemplates::changes(cx) to invalidate cached content. Saved Prompts emits this after CRUD/reload; Skills emits it after source registration, refresh, reordering, enablement, deletion and rescans. The signal is an invalidation notice, not permission for a provider to write project files. Markdown Prompt refreshes its open template palette after invalidation. Project content changes only after Apply changes → Save, which resolves referenced templates again; notifications never write files in the background.

The host calls Plugin::connect_services(services, cx) after publishing exports. This lifecycle hook gives native consumers the live catalog without opening a pane. Markdown Prompt currently has no background synchronization worker: opening or closing a pane, restarting the app, and provider changes do not apply saved compositions to project files.

Native plugins export the asynchronous typed service from Plugin::services():

ServiceExport::new(PromptTemplates::new(|project_dir, cx| {
    // Read provider-owned state here; perform expensive work in the background.
    cx.background_executor().spawn(async move {
        Ok(vec![SavedPrompt {
            id: "brief".into(),
            title: "Project brief".into(),
            prompt: build_markdown(project_dir)?,
        }])
    })
}))

The optional project directory is the requesting pane's folder. A provider may return an error when its state is unavailable or updating. No open provider pane is required. Consumers discover enabled exports with context.services.all::<PromptTemplates>() and request fresh content using provider.list(context.project_dir.clone(), cx). Consumers validate nonempty, unique IDs and nonempty titles and cap each body and constructed output at 1 MiB.

Portable plugins can publish literal Markdown without executing code:

[[prompt_templates]]
id = "brief"
title = "My plugin brief"
prompt = """
## My plugin

Instructions for working with this plugin.
"""

The host publishes manifest templates through the same service and removes them on disable/uninstall. A provider should use either its manifest declaration or a native dynamic export; manifest declarations take precedence if both are present. Dynamic exports currently use the trusted native service contract; portable web plugins can contribute literal templates but do not yet have an asynchronous JavaScript template callback API.

Saved Prompts retains com.chartr.prompts and its original storage/service contract for compatibility. Markdown Prompt is a separate bundled native plugin, com.chartr.markdown-prompt, with no required provider dependencies. Its UI and file ownership rules are documented in Markdown Prompt.