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
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user