fix(vscode): harden reasoning shortcut routing
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user