Skip to content

Commit 5dd4d17

Browse files
committed
fix(ui): prevent undo crash + allow arbitrary step_types
- Clamp cursor after replaceAllContent to prevent invalid lineNumber - Wrap cursor listener in try-catch for undo edge case - Remove forceMoveMarkers to avoid stale cursor positions in undo stack - Change step_types from Literal["tool","llm"] to list[str] with examples
1 parent 06bd6bb commit 5dd4d17

2 files changed

Lines changed: 59 additions & 47 deletions

File tree

‎models/src/agent_control_models/controls.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class ControlScope(BaseModel):
7373
"Step types this control applies to (omit to apply to all types). "
7474
"Built-in types are 'tool' and 'llm'."
7575
),
76+
json_schema_extra={"items": {"examples": ["tool", "llm"]}},
7677
)
7778
step_names: list[str] | None = Field(
7879
default=None,

‎ui/src/core/page-components/agent-detail/modals/edit-control/json-editor-view.tsx‎

Lines changed: 58 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,15 @@ function replaceAllContent(
7676
const model = editor.getModel();
7777
if (!model) return;
7878
const fullRange = model.getFullModelRange();
79-
editor.executeEdits(source, [
80-
{ range: fullRange, text: newText, forceMoveMarkers: true },
81-
]);
79+
editor.executeEdits(source, [{ range: fullRange, text: newText }]);
80+
// Clamp cursor to valid range after content replacement
81+
const pos = editor.getPosition();
82+
if (pos) {
83+
const maxLine = model.getLineCount();
84+
if (pos.lineNumber > maxLine) {
85+
editor.setPosition({ lineNumber: maxLine, column: 1 });
86+
}
87+
}
8288
}
8389

8490
function reformatIfValid(
@@ -380,53 +386,58 @@ export const JsonEditorView = ({
380386
const disposable = editor.onDidChangeCursorPosition(() => {
381387
if (timeout) window.clearTimeout(timeout);
382388
timeout = window.setTimeout(() => {
383-
const pos = editor.getPosition();
384-
const model = editor.getModel();
385-
if (!pos || !model) return;
386-
if (pos.lineNumber < 1 || pos.lineNumber > model.getLineCount()) return;
387-
388-
const line = model.getLineContent(pos.lineNumber);
389-
const beforeCursor = line.substring(0, pos.column - 1);
390-
const afterCursor = line.substring(pos.column - 1);
391-
392-
// Detect string value (not property key)
393-
const quotesBefore = beforeCursor.split('"').length - 1;
394-
const isInString =
395-
quotesBefore % 2 === 1 && /^[^"]*"/.test(afterCursor);
396-
const isPropertyKey =
397-
isInString && /^\s*:/.test(afterCursor.replace(/^[^"]*"/, ''));
398-
399-
let shouldTrigger = false;
400-
401-
if (isInString && !isPropertyKey) {
402-
const openIdx = beforeCursor.lastIndexOf('"');
403-
const closeIdx = afterCursor.indexOf('"');
404-
const len =
405-
openIdx >= 0 && closeIdx >= 0
406-
? beforeCursor.length - openIdx - 1 + closeIdx
407-
: 999;
408-
409-
if (len <= 2) {
410-
shouldTrigger = true;
411-
} else if (monacoRef.current) {
412-
// Only trigger for longer strings if we have domain suggestions
413-
shouldTrigger =
414-
getJsonEditorCompletionItems(
415-
monacoRef.current,
416-
model,
417-
pos,
418-
autocompleteContext
419-
).length > 0;
389+
try {
390+
const pos = editor.getPosition();
391+
const model = editor.getModel();
392+
if (!pos || !model) return;
393+
if (pos.lineNumber < 1 || pos.lineNumber > model.getLineCount())
394+
return;
395+
396+
const line = model.getLineContent(pos.lineNumber);
397+
const beforeCursor = line.substring(0, pos.column - 1);
398+
const afterCursor = line.substring(pos.column - 1);
399+
400+
// Detect string value (not property key)
401+
const quotesBefore = beforeCursor.split('"').length - 1;
402+
const isInString =
403+
quotesBefore % 2 === 1 && /^[^"]*"/.test(afterCursor);
404+
const isPropertyKey =
405+
isInString && /^\s*:/.test(afterCursor.replace(/^[^"]*"/, ''));
406+
407+
let shouldTrigger = false;
408+
409+
if (isInString && !isPropertyKey) {
410+
const openIdx = beforeCursor.lastIndexOf('"');
411+
const closeIdx = afterCursor.indexOf('"');
412+
const len =
413+
openIdx >= 0 && closeIdx >= 0
414+
? beforeCursor.length - openIdx - 1 + closeIdx
415+
: 999;
416+
417+
if (len <= 2) {
418+
shouldTrigger = true;
419+
} else if (monacoRef.current) {
420+
// Only trigger for longer strings if we have domain suggestions
421+
shouldTrigger =
422+
getJsonEditorCompletionItems(
423+
monacoRef.current,
424+
model,
425+
pos,
426+
autocompleteContext
427+
).length > 0;
428+
}
420429
}
421-
}
422430

423-
// Blank line inside object
424-
if (!shouldTrigger && /^\s*,?\s*$/.test(line)) {
425-
shouldTrigger = true;
426-
}
431+
// Blank line inside object
432+
if (!shouldTrigger && /^\s*,?\s*$/.test(line)) {
433+
shouldTrigger = true;
434+
}
427435

428-
if (shouldTrigger) {
429-
editor.trigger('cursor', 'editor.action.triggerSuggest', {});
436+
if (shouldTrigger) {
437+
editor.trigger('cursor', 'editor.action.triggerSuggest', {});
438+
}
439+
} catch {
440+
// Ignore — can happen during undo when cursor references stale line numbers
430441
}
431442
}, 50);
432443
});

0 commit comments

Comments
 (0)