md

Extension design rules

Good zi extensions are small, inspectable, and kind to the user.

They do one useful thing, say what they are doing, and leave the user in control. They avoid hidden policy, surprising network calls, and clever abstractions that only the author can debug.

This page is guidance for humans and for zi when generating extensions.

Prefer documented host APIs over host details. Do not depend on TUI components, terminal input streams, mailbox payloads, provider runtime handles, transcript row objects, or render caches.

Keep load/register cheap and deterministic. Register capabilities at load time; do real work later, when a tool, command, or event is running with a live context.

Choose the smallest surface

Use tools for model-visible capabilities.

Use commands for direct user actions.

Use events for lifecycle policy, prompt/context rewriting, message watching, and tool/provider interception.

Use `ctx.ui` to publish UI intent.

Use `ctx.state` for durable per-extension session state.

Use `ctx.ai.complete` for side-channel model calls that should not mutate the transcript.

Use `zi.register_provider` for provider/model visibility, not for request rewriting. Use `before_provider_request` for request rewriting.

Capability guide

Need: add an action the model can call
Use tools.
Need: add a slash command for the user
Use commands.
Need: change the system prompt
Use `before_agent_start`.
Need: rewrite submitted user input
Use `input`.
Need: rewrite messages before a provider call
Use `context`.
Need: rewrite the provider request
Use `before_provider_request`.
Need: show feedback, status, progress, a report, or a prompt
Use context ui api.
Need: remember a per-session choice
Use context state api.
Need: inspect the transcript, attach notes, label important entries, or query labeled entries
Use context session api.
Need: ask a side-channel model question
Use `ctx.ai.complete`.
Need: expose a model/provider
Use providers.
Need: run a bounded OS command from extension code
Use `zi.system`.
Need: delegate work to a child zi run
Use spawn helper.

Canonical patterns

A model-visible tool has one job: accept structured parameters, perform the action, and return content. If the behavior cannot be described in one sentence, split it.

lua
return function(zi)
  zi.register_tool({
    name = "project_status",
    description = "Summarize project status.",
    parameters = { type = "object", properties = {} },
    execute = function(params, ctx)
      return { content = { { type = "text", text = "No status provider configured." } } }
    end,
  })
end

A slash command is for direct user intent. It should be safe to run because a person asked for it.

lua
return function(zi)
  zi.register_command({
    name = "note",
    description = "Save a session note.",
    handler = function(args, ctx)
      local ok = ctx.session.append_note({ kind = "manual", body = tostring(args or "") })
      if ctx.ui then ctx.ui.message(ok and "note saved" or "note failed") end
    end,
  })
end

A semantic message observer can attach durable session metadata without touching raw jsonl or UI rows. Prefer this kind of visible memory over private state when the information belongs to the session.

lua
zi.on("message", function(event, ctx)
  local message = event.message or {}
  if message.role == "user" and message.text and message.text:match("decision") then
    ctx.session.label(message.entry_id, "decision")
    ctx.session.append_note({
      kind = "observation",
      body = "possible decision",
      source_entry_id = message.entry_id,
    })
  end
end)

An event is for policy or reaction. Keep event behavior easy to explain when someone reads the session later.

lua
zi.on("message", function(event, ctx)
  local message = event.message or {}
  if message.role == "assistant" and ctx.ui then
    ctx.ui.message("assistant replied")
  end
end)

Kindness rules

Ask before destructive actions.

Show paths before changing files outside the current project.

Name models and providers when an extension changes them.

Prefer local files, local state, and project-relative paths when possible.

Do not hide network calls inside unrelated commands.

Return readable errors. A person should know what failed and what they can try next.

Keep generated extensions short enough to review.

Extension examples

hello.lua
Minimal tool registration.
commands.lua
Slash command with a host-owned report.
dynamic_tools.lua
Dynamic tool registration from an event or command.
prompt_customizer.lua
System prompt customization with before_agent_start.
status_line.lua
Turn lifecycle status publication.
message_watch.lua
Semantic message observer.
model_completion.lua
Model catalog inspection and ctx.ai.complete.
session_lifecycle.lua
Session lifecycle observation and cancellable pre-hooks.
session_notes.lua
Session note storage and retrieval.
auto_label.lua
Durable message labels, label queries, and entry lookup.
git_status.lua
Yieldable zi.system command execution with host-owned report output.
message.lua
Short feedback through ctx.ui.message.
question.lua, questionnaire.lua, timed_confirm.lua
Host-owned prompts.
custom_header.lua, widget_placement.lua, hidden_thinking_label.lua, titlebar.lua
Pi-mono parity examples rewritten onto host-owned semantic UI primitives.
input_transform.lua, permission_gate.lua
Input and permission-style interception patterns.
summarize.lua, handoff.lua, qna.lua
Workflow-shaped commands/tools built from the same primitives.