Add Cursor Agent engine + per-engine model picker#397
Add Cursor Agent engine + per-engine model picker#397hugobiais wants to merge 8 commits intobrowser-use:mainfrom
Conversation
Wraps `agent -p --output-format stream-json --stream-partial-output` so sessions can be driven by Cursor's CLI alongside Claude Code and Codex. Self-registers via the existing engine registry, so the EnginePicker and engineLogin IPC pick it up with no UI changes.
- Engine adapters expose listModels(); a 24h on-disk cache (engine-model-cache.json) lives in userData with in-flight request de-duplication - Picker dropdown gains a two-step Provider → Model navigation, per-engine selection persisted via localStorage, and selected model rendered as a subline under the engine name - SpawnContext / RunEngineOptions carry an optional model id through to runEngine and the session DB so a chosen model is honored end-to-end
Filtering with txt.trim() drops chunks that are just spaces/newlines, which can run adjacent words together when Cursor splits its partial output across deltas. Only skip truly empty strings now, and keep lastNarrative anchored to non-whitespace text.
The dropdown previously sized itself to the viewport, which made the pill's small window clip it and wasted space in the hub. Both views (provider + model) now share a deterministic 200px box; the inner list scrolls when entries don't fit. The picker auto-flips between opening up and opening down based on the toggle's distance from the viewport top, and exports its menu height as a constant so the pill can grow its window to exactly the room needed when the menu is open. Adds the model count to the search placeholder so the user knows how many models the catalog returned.
There was a problem hiding this comment.
5 issues found across 25 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/renderer/pill/pill.css">
<violation number="1" location="app/src/renderer/pill/pill.css:750">
P2: Removing the focus outline here leaves engine picker items without a visible keyboard focus indicator.</violation>
</file>
<file name="app/src/renderer/hub/EnginePicker.tsx">
<violation number="1" location="app/src/renderer/hub/EnginePicker.tsx:362">
P2: `labelMode="model"` hides the engine name but never renders the model label, so the picker toggle can end up with no visible text.</violation>
</file>
<file name="app/src/renderer/hub/TaskInput.tsx">
<violation number="1" location="app/src/renderer/hub/TaskInput.tsx:166">
P2: Selected models can be saved under the wrong engine because `onModelChange` uses the stale `engine` state instead of the engine being selected in the same interaction.</violation>
</file>
<file name="app/src/main/index.ts">
<violation number="1" location="app/src/main/index.ts:1135">
P2: Fallback model loading bypasses the TTL check and can return expired cached model lists whenever listModels() fails.</violation>
</file>
<file name="app/src/renderer/hub/ConnectionsPane.tsx">
<violation number="1" location="app/src/renderer/hub/ConnectionsPane.tsx:278">
P2: Cursor logout does not invalidate the 24h engine model cache, so stale `cursor-agent` model lists can survive sign-out and be reused by the next session.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
| outline: none; | ||
| box-shadow: none; |
There was a problem hiding this comment.
P2: Removing the focus outline here leaves engine picker items without a visible keyboard focus indicator.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/renderer/pill/pill.css, line 750:
<comment>Removing the focus outline here leaves engine picker items without a visible keyboard focus indicator.</comment>
<file context>
@@ -701,7 +741,14 @@ html, body {
+
+.engine-picker__item-select:focus,
+.engine-picker__item-select:focus-visible {
+ outline: none;
+ box-shadow: none;
}
</file context>
| outline: none; | |
| box-shadow: none; | |
| outline: 2px solid var(--color-fg-primary); | |
| outline-offset: 2px; |
| {!modelOnlyLabel && ( | ||
| <span className="engine-picker__name">{currentEngine?.displayName ?? '…'}</span> | ||
| )} |
There was a problem hiding this comment.
P2: labelMode="model" hides the engine name but never renders the model label, so the picker toggle can end up with no visible text.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/renderer/hub/EnginePicker.tsx, line 362:
<comment>`labelMode="model"` hides the engine name but never renders the model label, so the picker toggle can end up with no visible text.</comment>
<file context>
@@ -145,38 +323,75 @@ export function EnginePicker({ value, onChange, onOpenChange }: EnginePickerProp
{currentEngine && <EngineLogo id={currentEngine.id} />}
- <span className="engine-picker__name">{currentEngine?.displayName ?? '…'}</span>
+ <span className="engine-picker__label">
+ {!modelOnlyLabel && (
+ <span className="engine-picker__name">{currentEngine?.displayName ?? '…'}</span>
+ )}
</file context>
| {!modelOnlyLabel && ( | |
| <span className="engine-picker__name">{currentEngine?.displayName ?? '…'}</span> | |
| )} | |
| {modelOnlyLabel ? ( | |
| <span className="engine-picker__model">{currentModelLabel}</span> | |
| ) : ( | |
| <span className="engine-picker__name">{currentEngine?.displayName ?? '…'}</span> | |
| )} |
| }, []); | ||
|
|
||
| const onModelChange = useCallback((model: string | undefined) => { | ||
| setModelsByEngine((prev) => ({ ...prev, [engine]: model })); |
There was a problem hiding this comment.
P2: Selected models can be saved under the wrong engine because onModelChange uses the stale engine state instead of the engine being selected in the same interaction.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/renderer/hub/TaskInput.tsx, line 166:
<comment>Selected models can be saved under the wrong engine because `onModelChange` uses the stale `engine` state instead of the engine being selected in the same interaction.</comment>
<file context>
@@ -121,19 +147,26 @@ export const TaskInput = forwardRef<TaskInputHandle, TaskInputProps>(function Ta
}, []);
+ const onModelChange = useCallback((model: string | undefined) => {
+ setModelsByEngine((prev) => ({ ...prev, [engine]: model }));
+ storeSelectedModel(engine, model);
+ }, [engine]);
</file context>
| } | ||
| const listed = await adapter.listModels(); | ||
| if (listed.source === 'fallback' || listed.error) { | ||
| const stale = readEngineModelCache().entries[validated]; |
There was a problem hiding this comment.
P2: Fallback model loading bypasses the TTL check and can return expired cached model lists whenever listModels() fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/index.ts, line 1135:
<comment>Fallback model loading bypasses the TTL check and can return expired cached model lists whenever listModels() fails.</comment>
<file context>
@@ -1025,6 +1111,43 @@ app.whenReady().then(async () => {
+ }
+ const listed = await adapter.listModels();
+ if (listed.source === 'fallback' || listed.error) {
+ const stale = readEngineModelCache().entries[validated];
+ if (stale && !forceRefresh) {
+ return { ...stale, cached: true, error: listed.error };
</file context>
| @@ -3,6 +3,7 @@ import anthropicLogo from './anthropic-logo.svg'; | |||
| import claudeCodeLogo from './claude-code-logo.svg'; | |||
There was a problem hiding this comment.
P2: Cursor logout does not invalidate the 24h engine model cache, so stale cursor-agent model lists can survive sign-out and be reused by the next session.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/renderer/hub/ConnectionsPane.tsx, line 278:
<comment>Cursor logout does not invalidate the 24h engine model cache, so stale `cursor-agent` model lists can survive sign-out and be reused by the next session.</comment>
<file context>
@@ -213,6 +240,46 @@ export function ConnectionsPane({ embedded }: ConnectionsPaneProps): React.React
+ const handleCursorLogout = useCallback(async () => {
+ const api = window.electronAPI;
+ if (!api?.settings?.cursor?.logout) return;
+ const res = await api.settings.cursor.logout();
+ if (!res.opened) console.warn('[connections] cursor logout failed', res.error);
+ await refreshCursor();
</file context>
|
will take a look at this in a bit! |
Summary
agent) alongside Claude Code and Codex: login/auth, stream-json parsing, Connections-pane card, engine-picker logo.listModels(); results cached 24h inengine-model-cache.json(userData) with in-flight de-dup.SpawnContext/RunEngineOptionsso sessions honor the user's choice; per-engine selection persisted inlocalStorage.Test plan
Risk + rollback
New cache file under userData (auto-rebuilt if deleted). The `model?` field on `SpawnContext` / `RunEngineOptions` is optional; adapters fall back to their CLI default when unset.