Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 48 additions & 28 deletions ts/docs/architecture/completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ that eliminates client-side heuristics.
beneath it) decides where completions start (`startIndex`), what separates
them from the prefix (`separatorMode`), whether the list is exhaustive
(`closedSet`), and when to advance to the next hierarchical level
(`commitMode`). Clients never split input on spaces or guess token
boundaries.
The host provides a `direction` signal ("forward" or "backward") to
resolve structural ambiguity when the input is valid.
Clients never split input on spaces or guess token boundaries.

2. **Longest-match wins** — At every layer (grammar, construction cache,
grammar store merge), only completions anchored at the longest matched
Expand Down Expand Up @@ -43,7 +44,7 @@ User keystroke
│ State machine: IDLE → PENDING → ACTIVE │
│ Decides: reuse local trie OR re-fetch from backend │
└────────────────────────┬─────────────────────────────────────┘
│ dispatcher.getCommandCompletion(input)
│ dispatcher.getCommandCompletion(input, direction)
┌──────────────────────────────────────────────────────────────┐
│ Dispatcher getCommandCompletion() │
Expand Down Expand Up @@ -75,7 +76,7 @@ The return path carries `CommandCompletionResult`:
completions: CompletionGroup[];
separatorMode?: SeparatorMode; // "space" | "spacePunctuation" | "optional" | "none"
closedSet: boolean; // true → list is exhaustive
commitMode?: CommitMode; // "explicit" | "eager"
directionSensitive: boolean; // true → opposite direction would produce different results
}
```

Expand All @@ -99,12 +100,12 @@ grammar rules.
3. When `maxPrefixLength` advances, discard all shorter-prefix completions.
4. Categorize each state's outcome:

| Category | Condition | Completion source |
| ------------------ | ------------------------------------------- | ------------------------------ |
| 1 — Exact | Rule fully matched, prefix fully consumed | None (rule satisfied) |
| 2 — Clean partial | Prefix consumed, rule has remaining parts | Next part of rule |
| 3a — Dirty partial | Trailing text matches start of current part | Current part (prefix-filtered) |
| 3b — Dirty partial | Trailing text doesn't match | Offer current part |
| Category | Condition | Forward completion source | Backward completion source |
| ------------------ | ------------------------------------------- | ------------------------------ | ------------------------------ |
| 1 — Exact | Rule fully matched, prefix fully consumed | None | Last matched word/wildcard |
| 2 — Clean partial | Prefix consumed, rule has remaining parts | Next part of rule | Last matched part |
| 3a — Dirty partial | Trailing text matches start of current part | Current part (prefix-filtered) | Current part (prefix-filtered) |
| 3b — Dirty partial | Trailing text doesn't match | Current part | Last matched part |

5. Multi-word string parts use `tryPartialStringMatch()` to offer one word
at a time instead of the entire phrase.
Expand Down Expand Up @@ -158,7 +159,6 @@ type CompletionGroups = {
matchedPrefixLength?: number; // grammar override for startIndex
separatorMode?: SeparatorMode;
closedSet?: boolean;
commitMode?: CommitMode;
};
```

Expand All @@ -181,7 +181,15 @@ strongest requirement.
### 4. Dispatcher

**Package:** `packages/dispatcher`
**Entry point:** `getCommandCompletion(input, context)` → `CommandCompletionResult`
**Entry point:** `getCommandCompletion(input, direction, context)` → `CommandCompletionResult`

The `direction` parameter is a `CompletionDirection` (`"forward" | "backward"`) provided
by the host. It resolves structural ambiguity when the full input is valid —
for example, when a typed command name matches both a complete subcommand and
a prefix of a longer one, direction tells the backend whether to proceed
forward (show what follows) or reconsider backward (show alternatives).
For free-form parameter values, the backend derives mid-token status from
the input's trailing whitespace instead.

Orchestrates command resolution, parameter parsing, agent invocation, and
built-in completions.
Expand All @@ -193,7 +201,7 @@ input
→ normalizeCommand() → resolveCommand()
→ ResolveCommandResult { descriptor, suffix, table, matched }
→ three-way branch:
├─ uncommitted command → sibling subcommands (separatorMode="none")
├─ reconsidering command → sibling subcommands (separatorMode="none")
├─ resolved descriptor → completeDescriptor()
│ ├─ parseParams(suffix, partial=true) → ParseParamsResult
│ ├─ resolveCompletionTarget() → CompletionTarget
Expand All @@ -206,12 +214,12 @@ input

**`resolveCompletionTarget`** — pure decision function:

| Spec case | Condition | Behavior |
| --------- | -------------------------------------------- | ---------------------------------------------------- |
| 1 | `remainderLength > 0` (partial parse) | Offer what follows longest valid prefix |
| 3a-i | Full parse, no trailing space, string param | Editing free-form value → invoke agent, prefix-match |
| 3a-ii | Full parse, no trailing space, flag name | Uncommitted flag → offer flag alternatives |
| 3b | Full parse, trailing space (or fully quoted) | Offer completions for next parameter/flag |
| Spec case | Condition | Behavior |
| --------- | ------------------------------------------------- | ---------------------------------------------------- |
| 1 | `remainderLength > 0` (partial parse) | Offer what follows longest valid prefix |
| 3a-i | Full parse, no trailing whitespace, string param | Editing free-form value → invoke agent, prefix-match |
| 3a-ii | Full parse, direction="backward", flag name | Reconsidering flag → offer flag alternatives |
| 3b | Full parse, direction="forward" (or fully quoted) | Offer completions for next parameter/flag |

**`computeClosedSet`** heuristic:

Expand Down Expand Up @@ -257,7 +265,8 @@ lifecycle of a completion interaction.
| A1 | No active session | Invalidation | Re-fetch |
| A2 | Input no longer extends anchor | Invalidation | Re-fetch |
| A3 | Non-separator char typed when separator required | Invalidation | Re-fetch |
| B4 | Unique match + eager commit mode | Navigation | Re-fetch next level |
| A7 | Direction changed on direction-sensitive result | Invalidation | Re-fetch |
| B4 | Unique match (always fires) | Navigation | Re-fetch next level |
| B5 | Separator typed after exact match | Navigation | Re-fetch next level |
| C6 | No trie matches + open set | Discovery | Re-fetch |
| — | Trie has matches | — | Reuse locally |
Expand Down Expand Up @@ -320,15 +329,25 @@ text.
| `"optional"` | Separator accepted but not required | CJK / mixed-script grammars |
| `"none"` | No separator | `[spacing=none]` grammars |

### `CommitMode`
### `CompletionDirection`

The host-provided signal that resolves structural ambiguity when the input
is fully valid. Instead of the backend telling the client when to advance,
the client tells the backend which direction to complete.

| Value | Meaning | When the host sends it |
| ------------ | --------------------- | ----------------------------------------------------- |
| `"forward"` | User is moving ahead | Appending characters, typed separator, menu selection |
| `"backward"` | User is reconsidering | Backspacing, deleting |

Controls when a uniquely-satisfied completion triggers a re-fetch for the next
hierarchical level.
Direction is only consulted at structural ambiguity points (command-level
and flag-level resolution). For free-form parameter values the backend
uses the input's trailing whitespace to decide whether the last token is
complete.

| Value | Meaning | Use case |
| ------------ | ------------------------------------ | ------------------------------- |
| `"explicit"` | User must type delimiter to commit | Parameter completions (default) |
| `"eager"` | Re-fetch immediately on unique match | `@` command prefix |
Trigger B4 (unique match) always fires a re-fetch regardless of direction,
since the session can determine locally that the completion is uniquely
satisfied.

### `closedSet`

Expand All @@ -348,7 +367,8 @@ Merge rule: AND across sources (closed only if _all_ sources are closed).
The CLI (`packages/cli/src/commands/interactive.ts`) follows the same
contract but with simpler plumbing:

1. Sends full input to `dispatcher.getCommandCompletion(line)` (no
1. Sends full input and a `direction` (always `"forward"` for tab-completion)
to `dispatcher.getCommandCompletion(line, direction)` (no
token-boundary heuristics).
2. Uses `result.startIndex` as the readline filter position.
3. Prepends a space separator when `separatorMode` is `"space"` or
Expand Down
Loading
Loading