diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 41c621cb3..209553887 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/capability/rule.ts b/packages/coding-agent/src/capability/rule.ts index 652dc51c6..f698534ba 100644 --- a/packages/coding-agent/src/capability/rule.ts +++ b/packages/coding-agent/src/capability/rule.ts @@ -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; } diff --git a/packages/coding-agent/src/discovery/helpers.ts b/packages/coding-agent/src/discovery/helpers.ts index 817bc86db..24fdcc215 100644 --- a/packages/coding-agent/src/discovery/helpers.ts +++ b/packages/coding-agent/src/discovery/helpers.ts @@ -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, }; } diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index ecc6f14db..5cc6aac1b 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -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 {