feat(coding-agent): enabled per-rule interrupt mode overrides via frontmatter

- Added per-rule `interruptMode` override capability to TTSR interrupt logic via optional frontmatter field.
- Changed interrupt behavior to respect per-rule `interruptMode` settings with fallback to global `ttsr.interruptMode` configuration.
- Extended `Rule` and `RuleConfig` interfaces with optional `interruptMode` property for granular control.
- Updated rule discovery to extract and validate `interruptMode` from frontmatter with proper enum type checking.
This commit is contained in:
can1357
2026-03-14 14:13:09 +01:00
parent f766814d5a
commit 149f787fae
4 changed files with 23 additions and 13 deletions
+2 -1
View File
@@ -1,9 +1,9 @@
# Changelog
## [Unreleased]
### Added
- Added per-rule TTSR interrupt mode override via `interruptMode` field in rule frontmatter to allow fine-grained control over when TTSR interrupts stream processing
- Added `task` model role to allow configuring a dedicated model for subtask execution via `modelRoles.task` setting
- Added `moveCursorToMessageEnd` and `moveCursorToMessageStart` prompt actions to navigate to the beginning and end of the entire message
- Added support for provider-level `compat` configuration to apply OpenAI compatibility settings across all models from a provider
@@ -14,6 +14,7 @@
### Changed
- Changed TTSR interrupt logic to respect per-rule `interruptMode` settings, falling back to global `ttsr.interruptMode` when rule-level override is not specified
- Reorganized settings tabs from 12 tabs (display, agent, input, tools, config, services, bash, lsp, ttsr, status) to 8 focused tabs (appearance, model, interaction, context, editing, tools, tasks, providers) for improved discoverability
- Consolidated status line settings into the Appearance tab instead of a separate Status tab
- Reorganized sampling parameters (temperature, topP, topK, minP, presencePenalty, repetitionPenalty) into the Model tab
@@ -20,6 +20,8 @@ export interface RuleFrontmatter {
condition?: string | string[];
/** New key for TTSR stream scope. */
scope?: string | string[];
/** Per-rule TTSR interrupt mode override. */
interruptMode?: "never" | "prose-only" | "tool-only" | "always";
[key: string]: unknown;
}
@@ -43,6 +45,8 @@ export interface Rule {
condition?: string[];
/** Optional stream scope tokens (for example: text, thinking, tool:edit(*.ts)). */
scope?: string[];
/** Per-rule TTSR interrupt mode override (falls back to global ttsr.interruptMode). */
interruptMode?: "never" | "prose-only" | "tool-only" | "always";
/** Source metadata */
_source: SourceMeta;
}
@@ -163,6 +163,11 @@ export function buildRuleFromMarkdown(
}
const resolvedName = options?.ruleName ?? name.replace(options?.stripNamePattern ?? /\.(md|mdc)$/, "");
const rawMode = frontmatter.interruptMode;
const interruptMode: Rule["interruptMode"] =
rawMode === "never" || rawMode === "prose-only" || rawMode === "tool-only" || rawMode === "always"
? rawMode
: undefined;
return {
name: resolvedName,
path: filePath,
@@ -172,6 +177,7 @@ export function buildRuleFromMarkdown(
description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
condition,
scope,
interruptMode,
_source: source,
};
}
@@ -593,7 +593,7 @@ export class AgentSession {
this.#addPendingTtsrInjections(matches);
if (this.#shouldInterruptForTtsrMatch(matchContext)) {
if (this.#shouldInterruptForTtsrMatch(matches, matchContext)) {
// Abort the stream immediately — do not gate on extension callbacks
this.#ttsrAbortPending = true;
this.#ensureTtsrResumePromise();
@@ -1027,18 +1027,17 @@ export class AgentSession {
return -1;
}
#shouldInterruptForTtsrMatch(matchContext: TtsrMatchContext): boolean {
const mode = this.#ttsrManager?.getSettings().interruptMode ?? "always";
if (mode === "never") {
return false;
#shouldInterruptForTtsrMatch(matches: Rule[], matchContext: TtsrMatchContext): boolean {
const globalMode = this.#ttsrManager?.getSettings().interruptMode ?? "always";
for (const rule of matches) {
const mode = rule.interruptMode ?? globalMode;
if (mode === "never") continue;
if (mode === "prose-only" && (matchContext.source === "text" || matchContext.source === "thinking"))
return true;
if (mode === "tool-only" && matchContext.source === "tool") return true;
if (mode === "always") return true;
}
if (mode === "prose-only") {
return matchContext.source === "text" || matchContext.source === "thinking";
}
if (mode === "tool-only") {
return matchContext.source === "tool";
}
return true;
return false;
}
#queueDeferredTtsrInjectionIfNeeded(assistantMsg: AssistantMessage): void {