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
//
@@ -11,7 +11,7 @@ import {
isQuestioning,
isPathMention,
optionPeriodEdit,
matchesOptionPeriodInput,
optionPeriodInput,
canRestoreOptionPeriodEdit,
} from "../../webview-ui/src/components/chat/prompt-input-utils"
@@ -165,24 +165,11 @@ describe("Option+Period input", () => {
isComposing: false,
}
it("records the exact native edit at the caret", () => {
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", () => {
it("records the value and selection before native input", () => {
expect(optionPeriodEdit(key, "hello", 1, 4, "backward")).toEqual({
before: "hello",
after: "h≥o",
start: 1,
end: 4,
pos: 2,
direction: "backward",
})
})
@@ -195,18 +182,59 @@ describe("Option+Period input", () => {
expect(optionPeriodEdit({ ...key, isComposing: true }, "", 0, 0, "none")).toBeUndefined()
})
it("matches only the corresponding native insertion", () => {
const edit = optionPeriodEdit(key, "a≥b", 3, 3, "none")!
expect(matchesOptionPeriodInput(edit, { inputType: "insertText", data: "≥" }, "a≥b≥", 4, 4)).toBe(true)
expect(matchesOptionPeriodInput(edit, { inputType: "insertFromPaste", data: "≥" }, "a≥b≥", 4, 4)).toBe(false)
expect(matchesOptionPeriodInput(edit, { inputType: "insertText", data: "." }, "a≥b.", 4, 4)).toBe(false)
it.each([
["≥", "he≥llo", 3],
["…", "he…llo", 3],
["🙂", "he🙂llo", 4],
])("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")!
expect(canRestoreOptionPeriodEdit(edit, "a≥b≥", 4, 4)).toBe(true)
expect(canRestoreOptionPeriodEdit(edit, "a≥b≥x", 5, 5)).toBe(false)
expect(canRestoreOptionPeriodEdit(edit, "a≥b≥", 3, 3)).toBe(false)
expect(
optionPeriodInput(edit, { inputType: "insertFromPaste", data: "≥", isComposing: false }, "a≥b≥", 4, 4),
).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) => {
saveTabMemory()
setActivePendingId(undefined)
setSelection(worktreeId)
const remembered = tabMemory()[worktreeId]
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 === "cycleAgentMode" && document.hasFocus()) cycleAgent(1)
else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) cycleAgent(-1)
else if (msg.action === "cycleReasoningEffort" && document.hasFocus() && !terms.activeId())
session.cycleVariant(activePendingId())
else {
else if (msg.action === "cycleReasoningEffort" && document.hasFocus()) {
const id = visibleTabId()
if (id && id !== REVIEW_TAB_ID && !isTerminalTabId(id)) session.cycleVariant(id)
} else {
// Handle jumpTo1 through jumpTo9
const match = /^jumpTo([1-9])$/.exec(msg.action ?? "")
if (match) jumpToItem(parseInt(match[1]!) - 1)
@@ -43,9 +43,10 @@ import {
isPromptBusy,
isPathMention,
optionPeriodEdit,
matchesOptionPeriodInput,
optionPeriodInput,
canRestoreOptionPeriodEdit,
type NativeEdit,
type NativeInput,
} from "./prompt-input-utils"
import type { ReviewComment, SendMessageFailedMessage, TextPart } from "../../types/messages"
import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdown"
@@ -171,7 +172,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
let highlightRef: HTMLDivElement | undefined
let dropdownRef: 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.
// Uses `on()` to track only draftKey — avoids re-running on every keystroke.
createEffect(
@@ -547,31 +548,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
function restoreNativeInput() {
const edit = native
const state = native
const edit = state?.input
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)) {
native = undefined
return
}
native = undefined
preEnhanceText = edit.enhanced
preEnhanceText = state.enhanced
ref.value = edit.before
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 target = e.target as HTMLTextAreaElement
const val = target.value
const edit = native
const matches =
edit &&
matchesOptionPeriodInput(edit, e, val, target.selectionStart ?? val.length, target.selectionEnd ?? val.length)
if (!matches) native = undefined
if (matches) {
edit.inserted = true
if (edit.command) {
const state = native
const input = state
? optionPeriodInput(state.edit, e, val, target.selectionStart ?? val.length, target.selectionEnd ?? val.length)
: undefined
if (!input) native = undefined
if (state && input) {
state.input = input
if (state.command) {
restoreNativeInput()
return
}
@@ -585,7 +587,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const edit = ref
? optionPeriodEdit(e, ref.value, ref.selectionStart, ref.selectionEnd, ref.selectionDirection)
: 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
if (e.key === "z" && (e.metaKey || e.ctrlKey) && !e.shiftKey && preEnhanceText !== null) {
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 handleOpenIndexingSettings = () => {
@@ -1031,7 +1038,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
value={text()}
onInput={handleInput}
onKeyDown={handleKeyDown}
onKeyUp={syncGhost}
onKeyUp={handleKeyUp}
onPaste={handlePaste}
onCompositionStart={() => {
native = undefined
@@ -52,13 +52,16 @@ export function atEnd(start: number, end: number, len: number): boolean {
export interface NativeEdit {
before: string
after: string
start: number
end: number
pos: number
direction: "forward" | "backward" | "none"
}
export interface NativeInput extends NativeEdit {
after: string
pos: number
}
export function optionPeriodEdit(
event: Pick<KeyboardEvent, "code" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey" | "isComposing">,
value: string,
@@ -68,33 +71,25 @@ export function optionPeriodEdit(
): NativeEdit | undefined {
if (event.code !== "Period" || !event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.isComposing)
return undefined
return {
before: value,
after: `${value.slice(0, start)}${value.slice(end)}`,
start,
end,
pos: start + 1,
direction,
}
return { before: value, start, end, direction }
}
export function matchesOptionPeriodInput(
export function optionPeriodInput(
edit: NativeEdit,
event: Pick<InputEvent, "data" | "inputType">,
event: Pick<InputEvent, "data" | "inputType" | "isComposing">,
value: string,
start: number,
end: number,
) {
return (
event.inputType === "insertText" &&
event.data === "≥" &&
value === edit.after &&
start === edit.pos &&
end === edit.pos
)
): NativeInput | undefined {
const data = event.data
if (event.inputType !== "insertText" || event.isComposing || !data) return undefined
const after = `${edit.before.slice(0, edit.start)}${data}${edit.before.slice(edit.end)}`
const pos = edit.start + data.length
if (value !== after || start !== pos || end !== pos) return undefined
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
}