Beta·every release is a pre-releaseAPIs and harness behavior are still changing·every release until v1 is a pre-release
uzev1 beta
Workspace

Extensions

What the workspace shows beside your agents, the official ones, how you allow them, and how to write your own

An extension is something the workspace shows beside your agents: a line in the bar under the agent area, a drawer beside the agent, a block of the sidebar, a band over its pane, a full-frame view. Harnesses never see one. Four are built in (Spec, Architect, Code and Review), and a plugin can bring more.

Official extensions

The official marketplace carries three, in one plugin, workspace. One install brings all three, and each stays off until you allow what it may reach and enable it:

uze add workspace@uze

This declares the plugin in the project's agents.yaml, so anyone who runs uze install there gets it too; uze add -m workspace@uze keeps it on this machine only, and uze remove workspace takes it out. To try them without a project of your own, ask any agent for /uze:try.

  • A GSD milestone beside the agent: the step it is at, the next one a click away, and a checkpoint answered over its pane.

    Shows in
    bardrawerband
    May
    Read the checkout, type into an agent once you confirm, and notify you. Enabled one project at a time.
  • focus

    README

    A pomodoro: the time left in the bar, a sound when a session ends, and the sessions of the last seven days.

    Shows in
    bardrawer
    May
    Keep its sessions on this machine and notify you.
  • contributions

    README

    The repository's commits as a contribution graph, day by day, with the commits of the day you click.

    Shows in
    bardrawer
    May
    Run git, confined where the machine confines programs, and remember the day you chose.

The Extensions screen

Under management (Ctrl+O), Extensions lists every extension on this machine. The built-in ones wear Bundled, and those from uze's own marketplace wear Official. Select one to open its drawer: the plugin it comes from, whether it runs here, what it may reach, and its preferences, which you set right there.

An extension a plugin brings runs nothing until you allow it. Press Enter on it to enable it: a dialog names it and lists what it may reach, such as reading the checkout, running git or typing into an agent, and Allow and enable turns it on. Until then, where it would show, it says it needs consent.

  • Enabling one here turns it on in every project. One that reaches your agents is the exception: it is enabled one project at a time.
  • An update that wants more than you allowed is held and runs nothing until you allow it again. One that wants the same or less is applied as it comes.
  • A program an extension runs, such as git, is asked about on a step of its own, which says where it was found and whether it runs confined.
  • Only you can allow one. An agent cannot answer for you from its pane.

When you open a project whose plugins bring an extension that is not enabled there, the workspace offers it in one dialog, for that project or for every project. Putting the dialog away answers nothing: it is offered again the next time the project opens.

Turning one off

Under management (Ctrl+O), on Extensions, select one and press Enter to switch it off or back on. The built-in ones start on. Turning one off takes everything it adds with it at once: its button, its keys (they go back to the program in the pane), its entry in the action index (F1) and its sidebar section.

The choice for a built-in one is kept in ~/.uze/config.toml. From a terminal, uze config extension architect off does the same, and uze config extension lists them. You can also write it by hand:

[extensions]
architect = false

An extension's own preferences are edited in the same place and kept beside that choice, under its id. They stay there when its plugin is removed, and a value the extension does not admit reads as its default.

[preferences.review]
mark-viewed-automatically = false

Writing an extension

An extension never draws. It declares what it shows and where its data comes from, and the workspace draws it, in the operator's theme and glyphs, with the same keys and mouse as everything else there. Its logic runs in a bounded script engine that reaches only what the operator allowed.

The uze:author skill writes extensions too:

/uze:author add an extension to my plugin that lists the open items of TODO.md

It cannot allow the extension for you: it tells you what the extension asks for and where to allow it.

Its files

count.uze
extension.kdl
items.uze
list.uze
plugin.json

Each extension is a directory under extensions/, named by its id, and a plugin may carry several.

FileWhat it holds
extension.kdlThe manifest, in KDL: the surfaces, where their data comes from, the actions, and the grants it asks for
*.uzeComponents: a <view> block in KDL, built from the catalog's nodes, a <script> block in Rhai, or both
fixtures/*.jsonA sample of what a source answers, for tests and for a view with no script
tests/*.test.kdlTests the check runs, and their snapshots

To scaffold one into a plugin you are writing:

uze agent plugin create my-plugin --market my-market --extension todo

It writes a manifest with a full-frame view and a sidebar section, the component they draw, a fixture, and tests that pass. The first time it writes an extension into a marketplace, it also adds this line to the marketplace's .gitattributes, creating the file if there is none, so forges highlight components as KDL:

*.uze linguist-language=KDL

Where it shows

A surface names its slot:

SlotWhere
barOne line in the bar under the agent area, on the left or the right. A click opens the drawer or view it names
drawerA panel on a side of the agent area, beside the pane or floating over it
viewThe full frame, opened from its door in the bar
sectionA block of the sidebar
statusOne row of the sidebar's header
bandOne to six rows along the top or bottom of each agent's pane
overlayA card over the frame, opened by an action and closed by Esc

A band's rows are taken before the agent's pane is sized, and a floating drawer keeps the agent's terminal at its size, so neither makes the agent rewrap what it printed.

A worked example

This extension counts the open items of the checkout's TODO.md in the bar, and lists them in a drawer a click on the count opens.

extensions/todo/extension.kdl
protocol 1
id "todo"
name "Todo"
description "The open items of the checkout's TODO.md"

wants "read:checkout" "clipboard"

surface "count" slot="bar" view="count.uze" source="items" opens="list"
surface "list" slot="drawer" view="list.uze" source="items" title="Todo"

source "items" {
    script "items.uze"
    input file="TODO.md"
    applies-when "TODO.md"
    refresh-on "paths.changed" "TODO.md"
}

action "copy" label="Copy the item" on="list" key="ctrl-y" {
    clipboard "{text}"
}

The source runs the script in items.uze with its input, only in a checkout that has a TODO.md, and again whenever that file changes. The action copies the selected item. wants lists every grant the extension asks for: the check refuses an action or a script that needs one it does not list.

Each view binds the source's data with {field}:

extensions/todo/count.uze
<view>
Row gap=1 {
    Icon "check" role="info"
    Text "{count} open"
}
</view>
extensions/todo/list.uze
<view>
Stack gap=1 {
    Text "{count} open in TODO.md" role="muted"
    List each="items" empty="Nothing open" {
        Text "{text}"
    }
}
</view>

A colour is always a role (muted, info, warning), never a value, and an icon is named, never typed as a glyph: the theme and the glyph set the operator chose decide how each one looks.

The script answers with an envelope whose data is what the views bind:

extensions/todo/items.uze
<script>
// Every line of the file that opens with `- [ ] `.
fn answer(input, context) {
    let open = [];
    for line in lines(read_checkout(input.file)) {
        if line.starts_with("- [ ] ") {
            open.push(#{ text: line.sub_string(6) });
        }
    }
    #{ data: #{ items: open, count: open.len() } }
}
</script>

read_checkout exists only because the manifest wants read:checkout. A script has no import or eval, and is stopped past its bounds: a million operations, 100 ms, 1 MiB strings.

A test opens a surface with a fixture in place of the script, and acts on it as the operator would:

extensions/todo/fixtures/items.json
{
  "count": 2,
  "items": [
    { "text": "Write the view" },
    { "text": "Check the plugin" }
  ]
}
extensions/todo/tests/todo.test.kdl
test "the bar counts the open items" {
    surface "count"
    given "items" fixture="fixtures/items.json"
    expect-text "2 open"
}

test "the drawer lists them, and ctrl+y copies one" {
    surface "list"
    given "items" fixture="fixtures/items.json"
    expect-text "Check the plugin"
    press "ctrl+y"
    expect-action "copy"
    snapshot "list"
}

Sources and actions

A source says where a surface's data comes from:

SourceAnswered by
fixtureA JSON file of the extension. Runs nothing, so it asks for no consent
scriptThe answer function of a component's <script>
runA program, given as an argv and never through a shell: a file of the extension, or a program an exec: grant names
agentsThe workspace: every agent of the open spaces and its state
eventsThe workspace: the last events the extension saw
residentA script kept running while its surface is shown, with its own clock and keys, for a game or a live view

A source asks again on a clock (every "30s", while a surface showing it is in view), when files beneath a path of the checkout change, when the checkout's branch, head or working tree changes, or when an agent starts or finishes a turn or a tool. An action is what a person can do on a surface: call a script function, ask a source again, copy, open a file of the checkout, open another surface, or type a prompt into an agent, which the workspace shows first and sends only to an agent that is ready. Inputs (Field, Select, Form) and preferences the operator sets on the Extensions screen feed both.

Installing a plugin does not let its extensions run: the operator allows each one, grant by grant, on the Extensions screen. Each grant in wants is one line of that question:

GrantLets the extension
read:checkoutRead files beneath the checkout's root
clipboard, open-pathCopy text, open a file of the checkout
exec:<program>Run that program
store, store:globalKeep a small JSON record per project, or one for the machine
notifySend a notification and a sound, rate-limited
agent:observeSee the workspace's agents and what they do
agent:promptType a prompt into an agent, shown first
net:<host>Fetch from that one host, over HTTPS
mcp:<server>/<tool>Call one tool of an MCP server a plugin delivers
residentKeep its script running while it is shown

No grant writes the checkout. Ask for the fewest that do the job.

  • A view fed only by fixtures needs no consent. Anything that runs waits until the operator allows it, and the surface says so meanwhile.
  • An update that asks for a new grant is held until it is allowed again.
  • A program is pinned to the path found when it was allowed and, where the machine can, confined: it reads the checkout and the system, writes only its own scratch folder and opens no socket. Linux, macOS and Windows each confine programs; uze doctor says how this machine does. How a program is confined →

Check, test and iterate

uze agent plugin check my-plugin                     # compile, bind and test every extension
uze agent plugin check my-plugin --update-snapshots  # write the snapshots a change you meant moved
uze agent plugin check my-plugin --run               # run the scripts against this checkout too
uze update my-plugin -m                              # deliver the change to the workspace

The check compiles every extension with the rules the workspace hosts it with, refuses a script that calls a host function its grants do not give it, binds each view to its fixture, and runs every tests/*.test.kdl. Each finding names its file, line and column, and how to fix it.

A test runs nothing the plugin ships, so it passes the same on any machine. It opens a surface, feeds it a fixture, then clicks, presses keys and expects text or an action. snapshot keeps the cells it drew, as text and colour roles rather than colours, so a theme change never breaks it. A snapshot missing or changed fails the check until --update-snapshots writes it, and the diff is yours to review.

Editor support

uze agent plugin lsp is a language server for extension.kdl, components and tests: completion and hover from the catalog, and the check's own diagnostics as you type. Inside a <script> block it answers nothing yet. Editors know no .uze language, so name one and point it at the server:

languages.toml
[language-server.uze]
command = "uze"
args = ["agent", "plugin", "lsp"]

[[language]]
name = "uze"
scope = "source.uze"
file-types = ["uze"]
language-servers = ["uze"]

[[language]]
name = "kdl"
language-servers = ["uze"]

The full reference

Every slot, source, grant, event, action and catalog node, with each node's properties, is in the extension reference, generated from the contract the workspace checks against. Its machine-readable form is schema.json. The uze:author skill's extension guide covers programs, resident scripts, the agents and the store in depth, and plugins/workspace holds the official extensions, with their tests, to read as examples.

On this page