fix(vscode): harden reasoning shortcut routing

This commit is contained in:
marius-kilocode
2026-06-19 12:57:09 +02:00
parent cc8c642540
commit e2084b589a
5 changed files with 116 additions and 64 deletions
@@ -761,6 +761,26 @@ describe("Agent Manager — VS Code import boundary", () => {
}) })
}) })
describe("Agent Manager reasoning effort routing", () => {
const app = fs.readFileSync(TSX_FILE, "utf-8")
it("clears a local pending tab when selecting a worktree", () => {
const start = app.indexOf("const selectWorktree")
const end = app.indexOf("const addSessionToCurrentWorktree", start)
const block = app.slice(start, end)
expect(block).toContain("setActivePendingId(undefined)")
})
it("targets the visible chat tab instead of stale pending state", () => {
const start = app.indexOf('msg.action === "cycleReasoningEffort"')
const end = app.indexOf("} else {", start)
const block = app.slice(start, end)
expect(block).toContain("visibleTabId()")
expect(block).toContain("session.cycleVariant(id)")
expect(block).not.toContain("activePendingId()")
})
})
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Provider chain parity — sidebar App.tsx vs AgentManagerApp.tsx // Provider chain parity — sidebar App.tsx vs AgentManagerApp.tsx
// //
@@ -11,7 +11,7 @@ import {
isQuestioning, isQuestioning,
isPathMention, isPathMention,
optionPeriodEdit, optionPeriodEdit,
matchesOptionPeriodInput, optionPeriodInput,
canRestoreOptionPeriodEdit, canRestoreOptionPeriodEdit,
} from "../../webview-ui/src/components/chat/prompt-input-utils" } from "../../webview-ui/src/components/chat/prompt-input-utils"
@@ -165,24 +165,11 @@ describe("Option+Period input", () => {
isComposing: false, isComposing: false,
} }
it("records the exact native edit at the caret", () => { it("records the value and selection before native input", () => {
expect(optionPeriodEdit(key, "hello", 2, 2, "none")).toEqual({
before: "hello",
after: "he≥llo",
start: 2,
end: 2,
pos: 3,
direction: "none",
})
})
it("records replacement of selected text", () => {
expect(optionPeriodEdit(key, "hello", 1, 4, "backward")).toEqual({ expect(optionPeriodEdit(key, "hello", 1, 4, "backward")).toEqual({
before: "hello", before: "hello",
after: "h≥o",
start: 1, start: 1,
end: 4, end: 4,
pos: 2,
direction: "backward", direction: "backward",
}) })
}) })
@@ -195,18 +182,59 @@ describe("Option+Period input", () => {
expect(optionPeriodEdit({ ...key, isComposing: true }, "", 0, 0, "none")).toBeUndefined() expect(optionPeriodEdit({ ...key, isComposing: true }, "", 0, 0, "none")).toBeUndefined()
}) })
it("matches only the corresponding native insertion", () => { it.each([
const edit = optionPeriodEdit(key, "a≥b", 3, 3, "none")! ["≥", "he≥llo", 3],
expect(matchesOptionPeriodInput(edit, { inputType: "insertText", data: "≥" }, "a≥b≥", 4, 4)).toBe(true) ["…", "he…llo", 3],
expect(matchesOptionPeriodInput(edit, { inputType: "insertFromPaste", data: "≥" }, "a≥b≥", 4, 4)).toBe(false) ["🙂", "he🙂llo", 4],
expect(matchesOptionPeriodInput(edit, { inputType: "insertText", data: "." }, "a≥b.", 4, 4)).toBe(false) ])("derives the native insertion from the input event for %s", (data, value, pos) => {
const edit = optionPeriodEdit(key, "hello", 2, 2, "none")!
expect(optionPeriodInput(edit, { inputType: "insertText", data, isComposing: false }, value, pos, pos)).toEqual({
...edit,
after: value,
pos,
})
}) })
it("restores only while the value and caret still match", () => { it("records replacement of selected text", () => {
const edit = optionPeriodEdit(key, "hello", 1, 4, "backward")!
expect(optionPeriodInput(edit, { inputType: "insertText", data: "…", isComposing: false }, "h…o", 2, 2)).toEqual({
...edit,
after: "h…o",
pos: 2,
})
})
it("rejects input that does not exactly match the native edit", () => {
const edit = optionPeriodEdit(key, "a≥b", 3, 3, "none")! const edit = optionPeriodEdit(key, "a≥b", 3, 3, "none")!
expect(canRestoreOptionPeriodEdit(edit, "a≥b≥", 4, 4)).toBe(true) expect(
expect(canRestoreOptionPeriodEdit(edit, "a≥b≥x", 5, 5)).toBe(false) optionPeriodInput(edit, { inputType: "insertFromPaste", data: "≥", isComposing: false }, "a≥b≥", 4, 4),
expect(canRestoreOptionPeriodEdit(edit, "a≥b≥", 3, 3)).toBe(false) ).toBeUndefined()
expect(
optionPeriodInput(edit, { inputType: "insertCompositionText", data: "≥", isComposing: false }, "a≥b≥", 4, 4),
).toBeUndefined()
expect(
optionPeriodInput(edit, { inputType: "insertText", data: "≥", isComposing: true }, "a≥b≥", 4, 4),
).toBeUndefined()
expect(
optionPeriodInput(edit, { inputType: "insertText", data: null, isComposing: false }, "a≥b", 3, 3),
).toBeUndefined()
expect(
optionPeriodInput(edit, { inputType: "insertText", data: "", isComposing: false }, "a≥b", 3, 3),
).toBeUndefined()
expect(
optionPeriodInput(edit, { inputType: "insertText", data: "≥", isComposing: false }, "a≥b≥x", 5, 5),
).toBeUndefined()
expect(
optionPeriodInput(edit, { inputType: "insertText", data: "≥", isComposing: false }, "a≥b≥", 3, 4),
).toBeUndefined()
})
it("restores only while the observed value and caret still match", () => {
const edit = optionPeriodEdit(key, "a≥b", 3, 3, "none")!
const input = optionPeriodInput(edit, { inputType: "insertText", data: "🙂", isComposing: false }, "a≥b🙂", 5, 5)!
expect(canRestoreOptionPeriodEdit(input, "a≥b🙂", 5, 5)).toBe(true)
expect(canRestoreOptionPeriodEdit(input, "a≥b🙂x", 6, 6)).toBe(false)
expect(canRestoreOptionPeriodEdit(input, "a≥b🙂", 4, 4)).toBe(false)
}) })
}) })
@@ -941,6 +941,7 @@ const AgentManagerContent: Component = () => {
const selectWorktree = (worktreeId: string) => { const selectWorktree = (worktreeId: string) => {
saveTabMemory() saveTabMemory()
setActivePendingId(undefined)
setSelection(worktreeId) setSelection(worktreeId)
const remembered = tabMemory()[worktreeId] const remembered = tabMemory()[worktreeId]
if (terms.hasRemembered(worktreeId, remembered)) return termHandlers.activate(remembered!) if (terms.hasRemembered(worktreeId, remembered)) return termHandlers.activate(remembered!)
@@ -1067,9 +1068,10 @@ const AgentManagerContent: Component = () => {
else if (msg.action === "newTerminal") termHandlers.requestNew() else if (msg.action === "newTerminal") termHandlers.requestNew()
else if (msg.action === "cycleAgentMode" && document.hasFocus()) cycleAgent(1) else if (msg.action === "cycleAgentMode" && document.hasFocus()) cycleAgent(1)
else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) cycleAgent(-1) else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) cycleAgent(-1)
else if (msg.action === "cycleReasoningEffort" && document.hasFocus() && !terms.activeId()) else if (msg.action === "cycleReasoningEffort" && document.hasFocus()) {
session.cycleVariant(activePendingId()) const id = visibleTabId()
else { if (id && id !== REVIEW_TAB_ID && !isTerminalTabId(id)) session.cycleVariant(id)
} else {
// Handle jumpTo1 through jumpTo9 // Handle jumpTo1 through jumpTo9
const match = /^jumpTo([1-9])$/.exec(msg.action ?? "") const match = /^jumpTo([1-9])$/.exec(msg.action ?? "")
if (match) jumpToItem(parseInt(match[1]!) - 1) if (match) jumpToItem(parseInt(match[1]!) - 1)
@@ -43,9 +43,10 @@ import {
isPromptBusy, isPromptBusy,
isPathMention, isPathMention,
optionPeriodEdit, optionPeriodEdit,
matchesOptionPeriodInput, optionPeriodInput,
canRestoreOptionPeriodEdit, canRestoreOptionPeriodEdit,
type NativeEdit, type NativeEdit,
type NativeInput,
} from "./prompt-input-utils" } from "./prompt-input-utils"
import type { ReviewComment, SendMessageFailedMessage, TextPart } from "../../types/messages" import type { ReviewComment, SendMessageFailedMessage, TextPart } from "../../types/messages"
import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdown" import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdown"
@@ -171,7 +172,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
let highlightRef: HTMLDivElement | undefined let highlightRef: HTMLDivElement | undefined
let dropdownRef: HTMLDivElement | undefined let dropdownRef: HTMLDivElement | undefined
let slashDropdownRef: HTMLDivElement | undefined let slashDropdownRef: HTMLDivElement | undefined
let native: (NativeEdit & { inserted: boolean; command: boolean; enhanced: string | null }) | undefined let native: { edit: NativeEdit; input?: NativeInput; command: boolean; enhanced: string | null } | undefined
// Save/restore input text when switching sessions. // Save/restore input text when switching sessions.
// Uses `on()` to track only draftKey — avoids re-running on every keystroke. // Uses `on()` to track only draftKey — avoids re-running on every keystroke.
createEffect( createEffect(
@@ -547,31 +548,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
function restoreNativeInput() { function restoreNativeInput() {
const edit = native const state = native
const edit = state?.input
const ref = textareaRef const ref = textareaRef
if (!edit?.inserted || !edit.command || !ref || !document.hasFocus() || document.activeElement !== ref) return if (!edit || !state.command || !ref || !document.hasFocus() || document.activeElement !== ref) return
if (!canRestoreOptionPeriodEdit(edit, ref.value, ref.selectionStart, ref.selectionEnd)) { if (!canRestoreOptionPeriodEdit(edit, ref.value, ref.selectionStart, ref.selectionEnd)) {
native = undefined native = undefined
return return
} }
native = undefined native = undefined
preEnhanceText = edit.enhanced preEnhanceText = state.enhanced
ref.value = edit.before ref.value = edit.before
ref.setSelectionRange(edit.start, edit.end, edit.direction) ref.setSelectionRange(edit.start, edit.end, edit.direction)
syncInput(edit.before, edit.end) syncInput(edit.before, ref.selectionStart ?? edit.start)
} }
const handleInput = (e: InputEvent) => { const handleInput = (e: InputEvent) => {
const target = e.target as HTMLTextAreaElement const target = e.target as HTMLTextAreaElement
const val = target.value const val = target.value
const edit = native const state = native
const matches = const input = state
edit && ? optionPeriodInput(state.edit, e, val, target.selectionStart ?? val.length, target.selectionEnd ?? val.length)
matchesOptionPeriodInput(edit, e, val, target.selectionStart ?? val.length, target.selectionEnd ?? val.length) : undefined
if (!matches) native = undefined if (!input) native = undefined
if (matches) { if (state && input) {
edit.inserted = true state.input = input
if (edit.command) { if (state.command) {
restoreNativeInput() restoreNativeInput()
return return
} }
@@ -585,7 +587,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const edit = ref const edit = ref
? optionPeriodEdit(e, ref.value, ref.selectionStart, ref.selectionEnd, ref.selectionDirection) ? optionPeriodEdit(e, ref.value, ref.selectionStart, ref.selectionEnd, ref.selectionDirection)
: undefined : undefined
native = edit ? { ...edit, inserted: false, command: false, enhanced: preEnhanceText } : undefined native = edit ? { edit, command: false, enhanced: preEnhanceText } : undefined
// Undo enhanced prompt with Ctrl+Z / ⌘Z // Undo enhanced prompt with Ctrl+Z / ⌘Z
if (e.key === "z" && (e.metaKey || e.ctrlKey) && !e.shiftKey && preEnhanceText !== null) { if (e.key === "z" && (e.metaKey || e.ctrlKey) && !e.shiftKey && preEnhanceText !== null) {
e.preventDefault() e.preventDefault()
@@ -674,6 +676,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
} }
const handleKeyUp = (e: KeyboardEvent) => {
if (native && !native.input && e.code === "Period" && e.altKey) native = undefined
syncGhost()
}
const canEnhance = () => !isBusy() && !isDisabled() && !enhancing() const canEnhance = () => !isBusy() && !isDisabled() && !enhancing()
const handleOpenIndexingSettings = () => { const handleOpenIndexingSettings = () => {
@@ -1031,7 +1038,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
value={text()} value={text()}
onInput={handleInput} onInput={handleInput}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onKeyUp={syncGhost} onKeyUp={handleKeyUp}
onPaste={handlePaste} onPaste={handlePaste}
onCompositionStart={() => { onCompositionStart={() => {
native = undefined native = undefined
@@ -52,13 +52,16 @@ export function atEnd(start: number, end: number, len: number): boolean {
export interface NativeEdit { export interface NativeEdit {
before: string before: string
after: string
start: number start: number
end: number end: number
pos: number
direction: "forward" | "backward" | "none" direction: "forward" | "backward" | "none"
} }
export interface NativeInput extends NativeEdit {
after: string
pos: number
}
export function optionPeriodEdit( export function optionPeriodEdit(
event: Pick<KeyboardEvent, "code" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey" | "isComposing">, event: Pick<KeyboardEvent, "code" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey" | "isComposing">,
value: string, value: string,
@@ -68,33 +71,25 @@ export function optionPeriodEdit(
): NativeEdit | undefined { ): NativeEdit | undefined {
if (event.code !== "Period" || !event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.isComposing) if (event.code !== "Period" || !event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.isComposing)
return undefined return undefined
return { return { before: value, start, end, direction }
before: value,
after: `${value.slice(0, start)}${value.slice(end)}`,
start,
end,
pos: start + 1,
direction,
}
} }
export function matchesOptionPeriodInput( export function optionPeriodInput(
edit: NativeEdit, edit: NativeEdit,
event: Pick<InputEvent, "data" | "inputType">, event: Pick<InputEvent, "data" | "inputType" | "isComposing">,
value: string, value: string,
start: number, start: number,
end: number, end: number,
) { ): NativeInput | undefined {
return ( const data = event.data
event.inputType === "insertText" && if (event.inputType !== "insertText" || event.isComposing || !data) return undefined
event.data === "≥" && const after = `${edit.before.slice(0, edit.start)}${data}${edit.before.slice(edit.end)}`
value === edit.after && const pos = edit.start + data.length
start === edit.pos && if (value !== after || start !== pos || end !== pos) return undefined
end === edit.pos return { ...edit, after, pos }
)
} }
export function canRestoreOptionPeriodEdit(edit: NativeEdit, value: string, start: number, end: number) { export function canRestoreOptionPeriodEdit(edit: NativeInput, value: string, start: number, end: number) {
return value === edit.after && start === edit.pos && end === edit.pos return value === edit.after && start === edit.pos && end === edit.pos
} }