Skip to content

Editor Integration

SASE exposes two editor-facing surfaces for prompt and xprompt editing:

  • sase lsp is the interactive editor path. Configure your editor to launch it as a stdio language server when you want completions, snippets, hover, diagnostics, code actions, and jump-to-definition while editing a prompt.
  • sase editor helper-bridge ... is the integration/debugging path. It reads one JSON request from stdin and writes one JSON response to stdout, so clients can fetch the same catalogs without implementing an LSP client.

Language Server

sase lsp starts the SASE xprompt language server over stdio. Run --version or --help in a terminal to verify the wrapper, then point your editor's LSP configuration at sase lsp:

sase lsp --version
sase lsp --help

In editor configuration terms, the command is sase and the argument list is ["lsp"].

The wrapper resolves the server command in this order:

  1. SASE_XPROMPT_LSP_CMD, parsed as a shell-style command for development.
  2. A sase-xprompt-lsp binary in the current Python environment's bin/ directory.
  3. sase-xprompt-lsp on PATH.
  4. The newer debug or release sase-xprompt-lsp binary under a sibling ../sase-core checkout.
  5. cargo run --manifest-path ../sase-core/Cargo.toml -p sase_xprompt_lsp -- when cargo is available and the sibling checkout has a Cargo.toml.

Use SASE_XPROMPT_LSP_CMD when you need to point the editor wrapper at a different source checkout or command, including a custom binary that should beat the managed venv copy. Full editable-install SASE updates reinstall the server into the uv-tool venv when pulled sase-core commits change. The Justfile uses SASE_CORE_DIR and SASE_LINKED_REPO_SASE_CORE_DIR (with the legacy SASE_SIBLING_REPO_* variables as fallbacks) for local sase-core build/install targets, but sase lsp itself does not read those variables.

The xprompt LSP binary is built and installed from the local sase-core checkout (just rust-lsp-install for development/editor validation). It is separate from the sase-core-rs Python wheel; installing or rebuilding that wheel alone does not install the language server.

The wrapper also exports installed package xprompt locations, bundled default config, plugin xprompt directories, and plugin config paths to the Rust server. It also materializes a local artifact-reference catalog under ~/.sase/xprompt_lsp/ by default. The server refreshes its xprompt catalog when the LSP session starts, keeps a short cache for completion requests, and exposes a sase.xpromptLsp.refreshCatalog command for clients that surface LSP commands. Artifact-reference diagnostics and semantic highlighting re-read their catalog on each request, so a launcher refresh or external rewrite is visible on the next editor pass; artifact-reference completion reads the same catalog through a short-lived cache keyed by the catalog file's signature (see LSP Features).

LSP Features

The xprompt language server is focused on prompt and xprompt editing:

Feature Behavior
XPrompt completion Completes #name, #!workflow, namespaced references, and slash-skill references from the structured catalog. Skills complete as #skill/<name> after # and as /<name> after a slash. Memory notes complete as #memory/<stem> and never appear in slash completion.
Project/Patch tags Completes +query at prompt offset zero or immediately after an ASCII space from enabled projects and active Patches, inserting canonical VCS workspace tags.
VCS ref roots Completes #gh:, #git:, and other registered VCS workflow ref roots from project, Patch, and namespace catalog rows.
VCS repositories Completes repository names after namespace slashes such as #gh:owner/ through the owning workspace provider.
Argument assistance Completes named arguments, path inputs, and bool values for typed xprompt inputs where the catalog exposes input metadata.
Directive completion Completes the shared directive matrix: directive names and aliases, fixed values, %model: catalog rows and provider drill-down, parenthesized %model(..., alias=...) keys, and %id / %clan / %wait(...) / %queue(...) / %hold(...) keyword rows and values. Static %if(should_run=...) and %hold assistance is always available; %if::, %proc, and type: code assistance appear only when the typed_launch_units beta flag is enabled. See Feature flags.
Model shortcuts Completes =alias and ==model at prompt/line start or after an ASCII space into canonical %m: values, using the same shared Rust filters and edit plans as sase's TUI. = is an LSP trigger character; manual completion also works inside a valid equals token. See Equals model shortcuts.
Artifact references Fuzzy-completes bare @ and @query tokens as canonical artifact kinds, adding local paths on a kind-prefix miss or manual completion request, then completes local payloads after @kind:, including local stitch references.
File completion Completes path-like tokens and recent file-history entries; @-prefixed local paths appear automatically when no artifact kind prefix-matches, or on manual invocation.
Snippets Offers SASE snippets after bare trigger words when the client advertises LSP snippet support.
Hover Shows xprompt metadata, descriptions, previews, source display paths, tags, and active input hints. Memory entries also show kind memory and the current UI's tier label (core/reference), which is the note's memory type.
Diagnostics Reports xprompt/directive issues, xprompt call arguments that are unknown (unknown_xprompt_arg), repeated (duplicate_xprompt_arg), or the wrong type for the declared input (invalid_xprompt_arg_type), plus malformed or unresolved filesystem-backed artifact references outside prompt literal zones.
Semantic highlighting Highlights xprompt reference names, directive names, and their argument lists, plus the kind, payload, and supported fragment of known artifact references and glossary phrases, outside prompt literal zones using standard LSP semantic tokens. See Semantic token legend.
Definition Jumps from xprompt and slash-skill references to real source files when the catalog provides a resolvable path, including the backing note for #memory/<stem>.

When a client enables LSP on-type formatting for (, the server shares the prompt input's argument shorthand edits. Typing ( after an argument-opening : removes that colon; typing it after :: plus zero or more ASCII spaces inserts () before the delimiter and leaves the caret inside the pair, for example #review:: body becomes #review():: body. The edits preserve authored spacing and suffix text, and the syntax/literal-region exclusions match the TUI prompt input.

Snippet completions come from the same registry sase's TUI uses: xprompts with snippet front matter plus user-defined ace.snippets, with ace.snippets winning on trigger collisions. The server asks the host helper bridge for that authoritative registry and falls back to native Rust loading only for simple xprompt snippets and configured ace.snippets when the helper is unavailable. In sase's TUI, that shared registry expands into nested snippet sessions: expanding a trigger while another snippet's tabstops are live visits the inner tabstops first, then returns to the remaining outer tabstops. LSP clients receive ordinary editor snippets from the same catalog, so placeholder navigation in external editors is handled by the editor.

Directive completion uses UTF-16 LSP ranges, so accepting a row replaces the correct token even after non-BMP characters earlier on the line. %wait: remains positional and never offers bead= or time=; those keywords are only in parenthesized %wait(...). capacity=, priority=/p=, and weight=/w= complete on %queue/%q only. Keyword menus omit duplicate and conflicting entries, and model keyword values omit a self-reference such as @medium while completing %model(..., medium=...). Static directive names, aliases, keyword rows, and fixed values still complete if the helper bridge cannot refresh dynamic catalogs; model, agent, bead, repository, and artifact inventories degrade independently.

Feature flags

The sase lsp wrapper snapshots the launch-related feature flags when it starts the server and passes them to the Rust process, so completion matches what a launch from the same machine would accept:

Flag Server environment Effect on editor assistance
typed_launch_units SASE_TYPED_LAUNCH_UNITS Beta. Enables %if::, %proc, and type: code assistance; they are hidden and rejected while it is off.
queue_capacity_budget SASE_QUEUE_CAPACITY_BUDGET Sunset (default on). %queue capacity is described as this launch's runner-capacity budget; with the flag off, capacity help describes the older weighted-load threshold and %q: also suggests 0.

The server reads these values once, so restart the LSP session after changing a flag.

Equals model shortcuts

Typing = in an eligible prompt document requests model shortcut completion through the same Rust contracts sase's TUI uses. A valid equals token sits at prompt offset zero, at the start of a logical line, or immediately after an ASCII space, with the caret after the equals marker. Escaped or embedded equals signs, Markdown-style =text= / ==text== marker pairs, paths, inline and fenced code, disabled prompt regions, frontmatter, Jinja, placeholders, and directive-owned input stay ordinary text. The former *alias and **model syntax is a hard-cutover legacy spelling and remains ordinary text.

The =alias menu lists only effective implicit_alias and user_alias rows from the launcher-materialized model catalog, in canonical catalog order, with case-insensitive prefix matching. Accepting @large replaces the whole =query token, including any suffix to the right of a mid-token caret, with %m:@large. The ==model menu lists concrete model rows only, including provider-qualified matches such as ==codex/g; accepting gpt-5.6-sol replaces the whole ==query token with %m:gpt-5.6-sol.

At prompt or line end the expansion appends one ASCII space; before a tab it appends none; before an existing ASCII space it consumes and reinserts that first space so later whitespace is preserved and the caret lands after the first space.

A valid shortcut context with no matching rows returns an empty incomplete completion list instead of falling through to prose, file, or snippet rows. Missing or malformed catalogs do the same. The list is isIncomplete with filterText set to the typed equals prefix so clients re-request as the query changes instead of dropping the row because the source text differs from the inserted %m: value. The catalog is a launch-time snapshot: restart the LSP after config or plugin changes. Manual editor completion also works while the caret is in a valid equals token.

ace.prompt_completion.auto_directive_menu remains a setting for the prompt bar in sase's TUI and does not govern an editor client's trigger policy. The TUI prompt input uses these same shortcuts; see Prompt Input and Equals model shortcuts.

Artifact assistance is local-only. Before a : appears, @ completion withholds local file rows whenever the query prefix-matches an artifact kind (including bare @), and returns them automatically when no kind prefix-matches. A manually invoked completion request includes the file rows explicitly. Completion labels are the reference that gets inserted, such as @plan: and @src/. Document kinds (including dynamic sidecar roles), indexed artifact files, Patches, beads, agents, and stitches are enumerated or resolved from the selected project's local catalog roots and checkout paths. bead and agent payloads resolve locally from generated sidecar pages, and stitch payloads are enumerated from local git checkouts, excluding SDD sidecar repositories (plans, beads, agents, research) since their commits are machine-written bookkeeping rather than a human's recent work. A sidecar stitch reference still resolves when written out in full, such as @stitch:plans@<sha> — the exclusion only curates what completion offers. Historical aliases such as @commit:, @plans:, @chat:, and @bug: are not offered by completion. The LSP never contacts git hosts, issue trackers, or other network providers. Unknown @kind: text remains ordinary prose.

This canonical-only rule is specific to the editor LSP. sase's TUI prompt bar currently also lists recognized aliases and historical kinds, and its repository-history payload picker remains attached to @commit:; both @commit: and @stitch: resolve through the same launch resolver.

Matching is fuzzy and ranked on the server for every enumerated kind — document roles, indexed artifact files, Patches, beads, agents, and stitches — against the inserted payload, the row's title, and, for scoped rows, the qualified scope@title target. For example, @stitch:core@fix can match the sase-core repository scope and a commit subject containing fix in the same query. Likewise, @research:site finds @research:202607/sase_sites_hub_and_pages/sase_sites_hub_and_pages.md, @agent:sase-b3 finds @agent:bbugyi200.athena.sase-b3.5 from a mid-name fragment, and @file:panel finds a default:<hex> indexed file by its file name. Rows are grouped into tiers so a fuzzy hit never outranks a literal one, then ordered by score, provider rank where a provider declares one, shorter text, and case-insensitive text:

Tier Meaning Example query against 202607/sase_sites_hub_and_pages/…
0 query is a prefix of the primary text 202607/
1 query is a prefix of the basename segment sase_sites
2 query is a contiguous substring hub_and
3 query is an ordered subsequence site, shubp

An empty query is not ranked at all: each group keeps its provider order (builtin order for kinds, directories-before-files for paths, and provider order for payloads). Kind rows and the trailing partial of a local path are matched the same way, but a @ path token's directory portion stays exact, so @src/ still lists src/ and @src/fcb can still find src/…/_file_completion_base.py. Stitch SHA text is still part of the lowest fuzzy tier, so hex-like queries such as add can subsequence-match a SHA; subject and repository matches outrank those tier-3 rows.

Bounds are disclosed rather than silent. Filesystem-backed enumeration walks up to 5000 payloads per root, so a root larger than that is matched only over the rows the walk reached. Stitch completion reads at most 200 revisions per repository, gives each repository two seconds to answer, and merges at most 1000 commit rows across repositories. Matching then returns at most 200 rows per group to the editor. Whichever bound bites, the count of payloads left out is appended to every item's detail as at least N additional payloads not shown, on top of the list's isIncomplete: true. Because the walk is query-independent, the enumerated and titled inventory is cached in-process per project and per catalog signature (path, mtime, size) with a two-second TTL, so a keystroke re-ranks a warm corpus instead of re-walking the filesystem. A watched catalog write or the sase.xpromptLsp.refreshCatalog command invalidates it immediately.

Keeping server-ranked rows alive in the client is an explicit contract. Every artifact-reference item sets filterText to the reference text as typed (@research:site in the payload stage, @rsch in the kind stage) rather than to the inserted reference, because a client that prefix-filters @research:202607/… against @research:site would discard every fuzzy row. The response is a CompletionList with isIncomplete: true, which makes clients re-request on each keystroke instead of re-filtering a stale list and, in Neovim's native completion, disables the client's own fuzzy re-sort so the server's sortText order survives. Insertion is unaffected: each item carries the full reference in textEdit.newText.

Editors cannot highlight individual characters inside a completion label, so the "why is this row here" affordance moves into the preview. labelDetails.description keeps the group word (artifact kind, file, directory, or a payload kind such as stitch, patch, research, bead, or agent). labelDetails.detail adds the row's title when it differs from the label — a document's frontmatter title, an indexed file's basename, a Patch or bead title, an agent's short name, or a commit subject — and markdown documentation shows the matched payload with the matched runs wrapped in **, followed by that title on a second line. Stitch rows also include the bounded commit body when one is available.

Semantic token legend

Every semantic token uses a standard LSP token type, so editors style them with their normal semantic-token theme:

Prompt element Token type Modifiers
XPrompt reference name (#name) function
Directive name (%queue, %if, ...) macro
Argument delimiters and = operator
Argument key (count=) parameter
String argument value, including [[...]] text string
Numeric argument value number
Boolean argument value keyword
Artifact-reference kind namespace documentation for dynamic document-role references
Artifact-reference payload string documentation for dynamic document-role references
Artifact-reference fragment number documentation for dynamic document-role references
Glossary phrase type

Argument spans come from the same sase-core argument parser that colors arguments in sase's TUI prompt input, so both surfaces agree on roles. Argument tokens that belong to a key the xprompt does not declare carry the deprecated modifier; type mismatches and duplicate keys are reported only through diagnostics. A call that is still being typed (no closing parenthesis yet) gets structural tokens without validity markers. Multi-line [[...]] values are tokenized line by line with UTF-16 positions, and an artifact reference inside an argument value keeps its own kind and payload tokens.

sase-nvim keeps the glossary token's theme color and adds an overridable SaseGlossaryTerm underline on top through Neovim's LspTokenUpdate hook.

Helper Bridge

Editor integrations that do not need live LSP behavior can call fixed helper operations directly:

printf '{"schema_version":1,"project":"sase"}\n' | sase editor helper-bridge xprompt-catalog
printf '{"schema_version":1,"project":"sase"}\n' | sase editor helper-bridge snippet-catalog
printf '{"schema_version":1}\n' | sase editor helper-bridge agent-catalog
printf '{"schema_version":1}\n' | sase editor helper-bridge finalizer-catalog
printf '{"schema_version":1,"workflow":"gh","namespace":"sase-org"}\n' \
  | sase editor helper-bridge vcs-repo-catalog

xprompt-catalog returns the structured xprompt catalog used by mobile/editor clients, including insertion text, reference prefix, kind, tags, typed inputs, display/source fields, and definition_path when SASE can resolve a real file.

snippet-catalog returns the composed snippet registry:

  • XPrompt-derived snippets from markdown files with snippet front matter.
  • User snippets from ace.snippets in merged SASE config.
  • Valid trigger words only; user snippets override xprompt snippets on collision.
  • #[trigger] snippet references resolved after the xprompt/user merge.
  • Generated initial-capital aliases (fooFoo, uppercasing only the first character of the trigger and template) composed after that merge, so the registry matches sase's TUI. Explicit Foo definitions are never overwritten.

agent-catalog requires only {"schema_version":1} and reads across projects. It returns active and recent ordinary agent rows, de-duplicated by name, with status and project; monitor rows use kind: monitor. When the same artifact snapshot contains usable group metadata, the response adds the latest identifiable generation of each family and clan plus @tribe references derived from stored tribe assignments and clan declarations. Every row has name, kind, member_count, and display-ready detail; clan rows also have aggregate status.

Family rows carry a detail of family · N members by default. For the 20 families with the most recent activity — and only those, so the short-lived helper subprocess stays fast enough for interactive completion — the helper additionally tries to resolve an associated plan or bead. It checks concrete family members newest-first and uses the family root only as a legacy fallback, then enriches the row in three descending rungs:

  1. A plan or bead resolved with a title: detail becomes <kind> · <structure> · <title>, such as epic · 5 phases · 2 waves · Bead review hardening. kind is the raw tale/epic/plan/phase/task token. structure is the N phases · M waves fragment, present only when a plan exposes its phase list; bead-derived rows never have it.
  2. A kind resolved but no title (an untitled, missing, or unreadable plan file): a cleaned snippet of the family's launch prompt fills the title slot in that same shape.
  3. Nothing resolved but a usable prompt snippet exists: detail becomes family · N members · <snippet>.

Rungs 1 and 2 also attach Markdown documentation — the plan's goal, its epic phase list, or a phase/task bead's parent context, closed by a family · N members · <status> footer. Rung 3 and unenriched families have no documentation. Every family keeps its plain family · N members detail if resolution fails outright, and group enrichment is additive throughout, so missing plans or malformed legacy metadata never hide ordinary agent rows.

finalizer-catalog requires {"schema_version":1} and optionally accepts project. Unknown request fields are ignored. It returns the effective configured finalizer instances in policy order (required, remaining defaults, then optional, each group alphabetically) with value, provider_ref, required/default flags, after, max_attempts, documentation, and provenance_id. Malformed finalizer configuration returns status: error with an empty entries list instead of invented rows, and the builder never loads provider code.

vcs-repo-catalog requires a workflow and namespace, then asks that workflow's registered workspace provider for repositories. The response reports status, error_kind, message, provider_display, and whether returned cache data is stale. Each entry has a short name and a full ref such as sase-org/sase; replace the current VCS ref with ref rather than appending it after the namespace.

All five helper operations read one JSON object from stdin and write one compact JSON object to stdout. They are fixed catalog operations, not a general shell or filesystem bridge.

Authoring Snippets

Use ace.snippets for local trigger-word templates:

ace:
  snippets:
    fix: "Please fix the following issue:\n$0"
    review: "Review this code for correctness, performance, and style.\n$0"

Use xprompt front matter when a reusable prompt should also appear as a snippet:

---
name: review
snippet: true
input:
  path: path
---

Review {{ path }} for correctness, tests, and maintainability.

Required xprompt inputs become snippet tabstops. Optional inputs are pre-filled from defaults. XPrompts with complex Jinja control flow are skipped by snippet conversion so the generated editor template stays predictable.

Snippet templates can reuse other snippets by trigger with #[trigger]. Positional forms such as #[trigger(value)] and #[trigger:value] fill the referenced snippet's tabstops before the composed template is renumbered.

When the composed registry is used in sase's TUI, Tab moves forward through $1, $2, ... and $0, while Shift+Tab retreats through visited tabstops. Expanding a second trigger from inside an active snippet nests it instead of discarding the remaining outer stops.

Troubleshooting

Symptom Check
sase lsp cannot start Run sase lsp --version; run a full editable SASE update, build ../sase-core, or set SASE_XPROMPT_LSP_CMD.
Snippets do not appear Confirm the editor advertises LSP completionItem.snippetSupport; inspect sase editor helper-bridge snippet-catalog.
Completion catalog looks stale Restart the LSP session after changing installed plugin resources; for rewritten artifact catalogs, retry completion or diagnostics.
%proc rows are missing Enable the typed_launch_units beta flag, then restart the LSP session so the server sees the new flag snapshot.
Jump-to-definition is missing Check whether the catalog entry has a real definition_path; plugin or built-in virtual entries may only have display paths.
A user snippet is ignored Trigger names must contain only ASCII letters, digits, or _.