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.
- 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.
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.
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 launchingPortable 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 = trueselect 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.
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.
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-1through--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-schemeislightordark.
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.
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.
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.
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.
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.