Editor Integration¶
SASE exposes two editor-facing surfaces for prompt and xprompt editing:
sase lspis 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:
SASE_XPROMPT_LSP_CMD, parsed as a shell-style command for development.- A
sase-xprompt-lspbinary in the current Python environment'sbin/directory. sase-xprompt-lsponPATH.- The newer debug or release
sase-xprompt-lspbinary under a sibling../sase-corecheckout. cargo run --manifest-path ../sase-core/Cargo.toml -p sase_xprompt_lsp --whencargois available and the sibling checkout has aCargo.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
snippetfront matter. - User snippets from
ace.snippetsin 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 (
foo→Foo, uppercasing only the first character of the trigger and template) composed after that merge, so the registry matches sase's TUI. ExplicitFoodefinitions 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:
- A plan or bead resolved with a title:
detailbecomes<kind> · <structure> · <title>, such asepic · 5 phases · 2 waves · Bead review hardening.kindis the rawtale/epic/plan/phase/tasktoken.structureis theN phases · M wavesfragment, present only when a plan exposes its phase list; bead-derived rows never have it. - 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.
- Nothing resolved but a usable prompt snippet exists:
detailbecomesfamily · 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 _. |
Related Pages¶
- XPrompt reference for xprompt syntax, discovery order, typed inputs, snippets, and workflows.
- Integration APIs for the Python helper facade.
- Configuration for CLI flag and environment-variable reference.
- sase's TUI snippets for the in-TUI prompt widget behavior.