Skip to content

feat(ai): make benchmarking optional - #163

Merged
scotej merged 1 commit into
mainfrom
agent/optional-ai-benchmark
Aug 2, 2026
Merged

feat(ai): make benchmarking optional#163
scotej merged 1 commit into
mainfrom
agent/optional-ai-benchmark

Conversation

@scotej

@scotej scotej commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • separate model download, selection, and benchmarking so an installed model can be used without a benchmark
  • keep model and engine setup available while AI is off, with an explicit pre-enable warning offering Benchmark first, Turn on anyway, or Keep AI off
  • use the existing safe unbenchmarked runtime defaults: 5-second cadence, 90-second request timeout, and no p95 slowdown backoff
  • explain when to benchmark during onboarding and make the in-session AI status open AI settings directly
  • update the model picker, canonical docs, stories, and unit coverage for the optional flow

Safety

  • AI remains off by default
  • benchmarks require AI to be off at both the coordinator and sidecar layers, preventing the benchmark and live loop from sharing the process
  • selection, download completion, and model removal cannot bypass the unbenchmarked warning or race live AI
  • partial-download records cannot be selected as installed models

Validation

  • npm run build
  • npm run lint
  • npm run test — 1,045 tests
  • npm run format:check
  • token, string, migration, story, and contrast checks
  • npm run build-storybook

The local axe runner could not launch because this workspace has no Playwright browser; the protected GitHub CI check runs it before merge.

Closes #157

Summary by CodeRabbit

  • New Features

    • Added an optional AI benchmarking workflow with clearer setup guidance.
    • Separated model downloading, activation, benchmarking, and removal actions.
    • Added active-model indicators, benchmark status, quantization labels, and safe fallback behavior for unbenchmarked models.
    • Added AI setup validation, confirmation warnings, and improved onboarding guidance.
    • AI status in sessions can now open AI settings directly.
  • Bug Fixes

    • Prevented unsafe model changes during active AI sessions or setup operations.
    • Improved timing for AI sampling, alerts, and requests using benchmark results when available.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

AI setup now treats benchmarking as optional. Model download, activation, and benchmarking are separate actions. Unbenchmarked models use fallback cadence and timeout values. AI enablement validates readiness and shows a confirmation dialog when benchmark data is missing.

Changes

AI setup and benchmarking flow

Layer / File(s) Summary
Runtime contracts and model state
src/features/ai/sidecar.ts, src/features/ai/benchmark.ts, src/features/ai/benchmarkGate.ts, src/features/ai/modelStore.ts, tests/unit/ai-*
Sidecar startup distinguishes live and benchmark purposes. Model selection no longer depends on benchmark completion. Readiness and state-transition tests cover the new behavior.
Model picker lifecycle and actions
src/features/ai/ModelPicker.tsx, src/features/ai/ModelPickerContainer.tsx, src/stories/ModelPicker.stories.tsx
Download, activation, benchmarking, and removal use separate actions. Active-model indicators, busy-state handling, and unbenchmarked states are added.
AI enablement and settings integration
src/features/settings/categories/AiCategory.tsx, src/features/session/SessionView.tsx, src/features/onboarding/TutorialStep.tsx, src/strings.ts
AI settings validate model readiness and show benchmark warnings. Setup controls remain available while AI is disabled. Session navigation, onboarding content, and related messages are updated.
Fallback timing documentation and validation
ARCHITECTURE.md, PLAN.md, README.md, scripts/generate-benchmark-image.ts, src/features/ai/sampleLoop.ts, tests/unit/ai-sample-loop.test.ts
Documentation describes optional benchmarking, benchmark-derived timing, and fallback timing for unbenchmarked models.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SessionView
  participant AiCategory
  participant ModelPickerContainer
  participant useSidecarStore
  SessionView->>AiCategory: Open AI settings
  AiCategory->>ModelPickerContainer: Check active model and setup state
  AiCategory->>AiCategory: Show warning when benchmark data is missing
  AiCategory->>useSidecarStore: Enable live AI after confirmation
Loading

Possibly related PRs

  • scotej/studyvis#45: Modifies AI sidecar startup lifecycle and state handling.
  • scotej/studyvis#75: Modifies model management, benchmarking, session locking, and sidecar lifecycle handling.
  • scotej/studyvis#147: Modifies benchmark-derived request timeouts and fallback timing.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the main changes and validation, but it omits the required manual test, compatibility surfaces, gates, and merge style sections. Use the repository template and document manual test status, compatibility impact, locally run gates, and the selected merge style.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: making AI benchmarking optional.
Linked Issues check ✅ Passed The changes satisfy issue #157 by making benchmarking optional, adding pre-enable choices, and preserving local-only operation.
Out of Scope Changes check ✅ Passed The onboarding, settings, model-picker, documentation, stories, and tests directly support the optional benchmarking workflow.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch agent/optional-ai-benchmark

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@scotej
scotej merged commit 1fa4517 into main Aug 2, 2026
19 of 20 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements an “optional benchmark” AI setup flow: model download/selection can happen without benchmarking, and enabling AI now prompts with an explicit warning when the selected model is unbenchmarked (allowing benchmark-first or proceeding with safe defaults).

Changes:

  • Decouple model download, selection, and benchmarking; introduce readiness gating for AI enablement (including “unbenchmarked” warning state).
  • Update Settings → AI, Model Picker UI, onboarding tutorial copy, and in-session AI status affordance to better guide when/why to benchmark.
  • Extend unit coverage and docs to reflect the optional benchmark path and fallback runtime behavior.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/unit/ai-sidecar.test.ts Adds coverage for benchmark-vs-live sidecar start gating behavior.
tests/unit/ai-sample-loop.test.ts Updates I83 commentary to reflect optional benchmarking / active model selection changes.
tests/unit/ai-models.test.ts Expands tests for selecting models without benchmarks and preserving records across switches/forget.
tests/unit/ai-benchmark-gate.test.ts New unit tests for AI enable readiness mapping (loading/error/no-model/unbenchmarked/ready).
src/strings.ts Adds/updates copy for optional benchmarking flow, enablement warning dialog, and picker CTAs/status.
src/stories/ModelPicker.stories.tsx Updates story harness to new picker actions and adds active/unbenchmarked story states.
src/features/settings/categories/AiCategory.tsx Keeps model setup available while AI is off; adds pre-enable unbenchmarked warning dialog and busy gating.
src/features/session/SessionView.tsx Makes in-session AI status chip open AI settings directly (when handler present).
src/features/onboarding/TutorialStep.tsx Adds an “AI is optional” tutorial card and adjusts the grid layout.
src/features/ai/sidecar.ts Adds start-purpose concept and blocks benchmarking while AI is enabled (new error sentinel).
src/features/ai/sampleLoop.ts Clarifies fallback cadence semantics when no benchmark exists.
src/features/ai/modelStore.ts Introduces explicit selectModel and stops recordBenchmark from implicitly changing active selection.
src/features/ai/ModelPickerContainer.tsx Refactors orchestration: download/select/benchmark separated; adds busy callback + imperative benchmarkSelected handle.
src/features/ai/ModelPicker.tsx Updates UI to show separate Download / Use model / Benchmark actions, active-pill, and unbenchmarked messaging.
src/features/ai/index.ts Re-exports new readiness helpers and picker container handle types; exports new sidecar error/purpose type.
src/features/ai/benchmarkGate.ts New helper for mapping model-store state into AI enable readiness states.
src/features/ai/benchmark.ts Starts sidecar with explicit purpose: 'benchmark' to enforce benchmark-vs-live separation.
scripts/generate-benchmark-image.ts Updates script header comment to reflect optional benchmark framing.
README.md Updates user-facing AI explanation to reflect optional benchmarking and fallback defaults.
PLAN.md Updates V2 AI description to document optional benchmarks and explicit enablement warning behavior.
ARCHITECTURE.md Updates AI pipeline description to document optional benchmarking and fallback request-timeout behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +173 to 181
const aiEnabled = activeRuntime.getAiFeaturesEnabled()
if (purpose === 'benchmark' && aiEnabled) {
set({ lastError: ERR_BENCHMARK_REQUIRES_AI_OFF, status: 'idle' })
return null
}
if (purpose !== 'benchmark' && !aiEnabled) {
set({ lastError: ERR_AI_DISABLED, status: 'idle' })
return null
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/ai/ModelPickerContainer.tsx`:
- Around line 442-465: Wrap the post-download auto-selection in runDownload with
error handling around selectModel, using the existing onActivate pattern as
guidance. Ensure a selectModel persistence failure is caught and does not
prevent the subsequent updateCard idle reset, success toast, or cleanup that
clears setupBusy, while preserving the download-success flow and selected-state
behavior.

In `@src/features/session/SessionView.tsx`:
- Around line 1945-1958: Update the settings-opening button in the
onOpenSettings branch of SessionView to use focus-visible:outline-hidden instead
of outline-none alongside its existing focus ring, and add
aria-haspopup="dialog" to identify the dialog it opens. Leave the non-button
AiStatusChip rendering unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71b8d2ef-f81c-4836-a52f-eae302f68a3a

📥 Commits

Reviewing files that changed from the base of the PR and between 8e06695 and 8f025fe.

📒 Files selected for processing (21)
  • ARCHITECTURE.md
  • PLAN.md
  • README.md
  • scripts/generate-benchmark-image.ts
  • src/features/ai/ModelPicker.tsx
  • src/features/ai/ModelPickerContainer.tsx
  • src/features/ai/benchmark.ts
  • src/features/ai/benchmarkGate.ts
  • src/features/ai/index.ts
  • src/features/ai/modelStore.ts
  • src/features/ai/sampleLoop.ts
  • src/features/ai/sidecar.ts
  • src/features/onboarding/TutorialStep.tsx
  • src/features/session/SessionView.tsx
  • src/features/settings/categories/AiCategory.tsx
  • src/stories/ModelPicker.stories.tsx
  • src/strings.ts
  • tests/unit/ai-benchmark-gate.test.ts
  • tests/unit/ai-models.test.ts
  • tests/unit/ai-sample-loop.test.ts
  • tests/unit/ai-sidecar.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
  • GitHub Check: Installer (Windows)
  • GitHub Check: Installer (macOS)
  • GitHub Check: copilot-pull-request-reviewer
  • GitHub Check: Rust (macOS)
  • GitHub Check: Rust (Linux)
  • GitHub Check: Rust (Windows)
  • GitHub Check: Frontend
  • GitHub Check: Analyze (rust)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,rs}: The application must remain local-only: never add telemetry and never instruct users to paste model files or BIP39 mnemonics into an AI service.
Do not add comments unless the reason is non-obvious; identifiers should carry meaning and code should read top-to-bottom.

Files:

  • tests/unit/ai-benchmark-gate.test.ts
  • scripts/generate-benchmark-image.ts
  • src/features/ai/benchmarkGate.ts
  • src/features/onboarding/TutorialStep.tsx
  • tests/unit/ai-sample-loop.test.ts
  • src/features/ai/benchmark.ts
  • src/features/session/SessionView.tsx
  • src/features/ai/sampleLoop.ts
  • tests/unit/ai-models.test.ts
  • src/features/ai/sidecar.ts
  • tests/unit/ai-sidecar.test.ts
  • src/features/ai/modelStore.ts
  • src/stories/ModelPicker.stories.tsx
  • src/features/ai/index.ts
  • src/features/ai/ModelPickerContainer.tsx
  • src/features/settings/categories/AiCategory.tsx
  • src/features/ai/ModelPicker.tsx
  • src/strings.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript must remain strict and pass the build and type-check gates.

Files:

  • tests/unit/ai-benchmark-gate.test.ts
  • scripts/generate-benchmark-image.ts
  • src/features/ai/benchmarkGate.ts
  • src/features/onboarding/TutorialStep.tsx
  • tests/unit/ai-sample-loop.test.ts
  • src/features/ai/benchmark.ts
  • src/features/session/SessionView.tsx
  • src/features/ai/sampleLoop.ts
  • tests/unit/ai-models.test.ts
  • src/features/ai/sidecar.ts
  • tests/unit/ai-sidecar.test.ts
  • src/features/ai/modelStore.ts
  • src/stories/ModelPicker.stories.tsx
  • src/features/ai/index.ts
  • src/features/ai/ModelPickerContainer.tsx
  • src/features/settings/categories/AiCategory.tsx
  • src/features/ai/ModelPicker.tsx
  • src/strings.ts
tests/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit and integration tests; component tests are not currently supported because the test environment is Node without RTL/jsdom.

Files:

  • tests/unit/ai-benchmark-gate.test.ts
  • tests/unit/ai-sample-loop.test.ts
  • tests/unit/ai-models.test.ts
  • tests/unit/ai-sidecar.test.ts
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Keep changes narrowly scoped: do not refactor adjacent code or add abstractions for hypothetical future needs.
Use one focused change per commit with a Conventional Commit subject such as feat:, fix:, chore:, docs:, or ci:.
When implementation conflicts with PLAN.md, ARCHITECTURE.md, or DESIGN-SYSTEM.md, surface the conflict rather than silently deviating.

Files:

  • tests/unit/ai-benchmark-gate.test.ts
  • scripts/generate-benchmark-image.ts
  • src/features/ai/benchmarkGate.ts
  • src/features/onboarding/TutorialStep.tsx
  • tests/unit/ai-sample-loop.test.ts
  • src/features/ai/benchmark.ts
  • ARCHITECTURE.md
  • src/features/session/SessionView.tsx
  • src/features/ai/sampleLoop.ts
  • tests/unit/ai-models.test.ts
  • src/features/ai/sidecar.ts
  • tests/unit/ai-sidecar.test.ts
  • src/features/ai/modelStore.ts
  • src/stories/ModelPicker.stories.tsx
  • PLAN.md
  • README.md
  • src/features/ai/index.ts
  • src/features/ai/ModelPickerContainer.tsx
  • src/features/settings/categories/AiCategory.tsx
  • src/features/ai/ModelPicker.tsx
  • src/strings.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{ts,tsx}: All design-token values—colors, spacing, fonts, radii, shadows, motion, and z-index—must come from src/design/tokens.ts; do not use raw hex values, arbitrary px, or inline cubic-bezier values outside that file.
Keep toast and notification copy in src/strings.ts; prefer that module for user-facing strings, including JSX and aria-label text.
Meet WCAG AA contrast requirements for every text/background pairing in both themes; do not communicate information by color alone.
Reduced-motion support is a global kill switch, and new motion sites must be disabled by default when reduced motion is requested.
Peer wire formats and identity derivation are cross-version contracts; coordinate changes so older peers remain interoperable and stored data is not stranded.

Files:

  • src/features/ai/benchmarkGate.ts
  • src/features/onboarding/TutorialStep.tsx
  • src/features/ai/benchmark.ts
  • src/features/session/SessionView.tsx
  • src/features/ai/sampleLoop.ts
  • src/features/ai/sidecar.ts
  • src/features/ai/modelStore.ts
  • src/stories/ModelPicker.stories.tsx
  • src/features/ai/index.ts
  • src/features/ai/ModelPickerContainer.tsx
  • src/features/settings/categories/AiCategory.tsx
  • src/features/ai/ModelPicker.tsx
  • src/strings.ts
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add new documentation files unless explicitly requested; update canonical documents, CHANGELOG.md, or ISSUES.md only when justified.

Files:

  • ARCHITECTURE.md
  • PLAN.md
  • README.md
src/**/*.stories.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Every component must have a Storybook story, and Storybook stories must pass axe-core accessibility checks.

Files:

  • src/stories/ModelPicker.stories.tsx
🔇 Additional comments (29)
src/features/ai/sidecar.ts (1)

12-14: LGTM!

Also applies to: 32-33, 137-150, 171-178

src/features/ai/benchmark.ts (1)

1-1: LGTM!

Also applies to: 176-176

src/features/ai/benchmarkGate.ts (1)

1-22: LGTM!

src/features/ai/index.ts (1)

5-5: LGTM!

Also applies to: 11-15, 78-80, 178-181

src/features/ai/modelStore.ts (1)

44-45: LGTM!

Also applies to: 104-104, 196-204, 244-249

tests/unit/ai-benchmark-gate.test.ts (1)

1-94: LGTM!

tests/unit/ai-models.test.ts (1)

297-310: LGTM!

Also applies to: 312-322, 324-334, 336-354, 356-377, 379-395

tests/unit/ai-sidecar.test.ts (1)

8-8: LGTM!

Also applies to: 150-150, 163-202

src/features/ai/ModelPicker.tsx (2)

51-51: LGTM!

Also applies to: 72-78, 93-93, 159-208, 253-253, 273-296, 315-315, 365-366, 513-514, 523-524, 535-541, 597-597, 622-622


146-147: 📐 Maintainability & Code Quality

No changes needed. src/strings.ts already defines the strings.ai.picker keys used by ModelPicker.tsx.

src/features/ai/ModelPickerContainer.tsx (1)

3-18: LGTM!

Also applies to: 44-100, 121-131, 256-257, 303-317, 366-372, 466-540, 560-571, 584-595, 642-658

src/stories/ModelPicker.stories.tsx (1)

33-41: LGTM!

Also applies to: 74-81, 119-147, 166-166, 186-186, 202-202, 263-263

src/features/settings/categories/AiCategory.tsx (6)

1-1: LGTM!

Also applies to: 12-19, 32-36, 47-47


70-125: LGTM!


135-160: LGTM!

Also applies to: 172-173


193-193: LGTM!

Also applies to: 227-279


423-423: LGTM!

Also applies to: 433-505, 701-740


281-292: 🎯 Functional Correctness

No change needed.

benchmarkSelected() returns true after onBenchmark(selected) runs and returns false only when no model spec matches the active model ID. That no-selection path is already covered by the default picker UI states rather than an explicit failed phase.

src/features/onboarding/TutorialStep.tsx (1)

1-6: LGTM!

Also applies to: 68-68, 92-96

src/strings.ts (4)

138-139: LGTM!

Also applies to: 154-157


617-617: LGTM!


1309-1340: LGTM!


1746-1751: LGTM!

Also applies to: 1762-1769, 1778-1778, 1787-1788, 1798-1799, 1813-1829, 1858-1858

ARCHITECTURE.md (1)

386-392: LGTM!

Also applies to: 428-431, 457-459, 472-472

PLAN.md (1)

83-92: LGTM!

Also applies to: 105-105, 147-147

README.md (1)

155-170: LGTM!

scripts/generate-benchmark-image.ts (1)

3-3: LGTM!

src/features/ai/sampleLoop.ts (1)

102-113: LGTM!

tests/unit/ai-sample-loop.test.ts (1)

1921-1923: LGTM!

Comment on lines +442 to 465

// First install becomes the selected model, but never switch models as
// an in-flight download finishes while AI is enabled. That would bypass
// the unbenchmarked enable warning. A later model stays installed until
// the user explicitly clicks "Use model."
let selected = false
const modelState = useModelStore.getState()
const aiEnabled = useSettingsStore.getState().values.aiFeaturesEnabled
if (!modelState.activeModelId && !aiEnabled) {
await selectModel(spec.id)
selected = true
}

updateCard(spec.id, {
phase: 'idle',
downloadProgress: null,
errorMessage: null,
})
toast.success(
selected
? strings.ai.picker.installedAndSelectedToast(spec.displayName)
: strings.ai.picker.installedToast(spec.displayName)
)
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap the post-download selectModel call in error handling.

selectModel awaits persist, which rejects when the Tauri store write fails. runDownload does not catch that rejection, and the caller at line 477 invokes it with void runDownload(spec). Two consequences follow:

  1. Lines 455-459 never run, so the card stays in its last download phase instead of returning to idle.
  2. setupBusy stays true forever, so the container keeps reporting onBusyChange(true) and the AI toggle in AiCategory stays gated until the pane remounts.

The download itself already succeeded and recordInstalled already persisted at line 441. A failed auto-selection must not strand the card. onActivate at lines 514-535 already applies this pattern.

🐛 Proposed fix to isolate the auto-selection failure
       let selected = false
       const modelState = useModelStore.getState()
       const aiEnabled = useSettingsStore.getState().values.aiFeaturesEnabled
       if (!modelState.activeModelId && !aiEnabled) {
-        await selectModel(spec.id)
-        selected = true
+        try {
+          await selectModel(spec.id)
+          selected = true
+        } catch {
+          // The files are installed and recorded. A failed auto-selection
+          // leaves the user the explicit "Use model" action.
+        }
       }
 
       updateCard(spec.id, {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// First install becomes the selected model, but never switch models as
// an in-flight download finishes while AI is enabled. That would bypass
// the unbenchmarked enable warning. A later model stays installed until
// the user explicitly clicks "Use model."
let selected = false
const modelState = useModelStore.getState()
const aiEnabled = useSettingsStore.getState().values.aiFeaturesEnabled
if (!modelState.activeModelId && !aiEnabled) {
await selectModel(spec.id)
selected = true
}
updateCard(spec.id, {
phase: 'idle',
downloadProgress: null,
errorMessage: null,
})
toast.success(
selected
? strings.ai.picker.installedAndSelectedToast(spec.displayName)
: strings.ai.picker.installedToast(spec.displayName)
)
},
// First install becomes the selected model, but never switch models as
// an in-flight download finishes while AI is enabled. That would bypass
// the unbenchmarked enable warning. A later model stays installed until
// the user explicitly clicks "Use model."
let selected = false
const modelState = useModelStore.getState()
const aiEnabled = useSettingsStore.getState().values.aiFeaturesEnabled
if (!modelState.activeModelId && !aiEnabled) {
try {
await selectModel(spec.id)
selected = true
} catch {
// The files are installed and recorded. A failed auto-selection
// leaves the user the explicit "Use model" action.
}
}
updateCard(spec.id, {
phase: 'idle',
downloadProgress: null,
errorMessage: null,
})
toast.success(
selected
? strings.ai.picker.installedAndSelectedToast(spec.displayName)
: strings.ai.picker.installedToast(spec.displayName)
)
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/ai/ModelPickerContainer.tsx` around lines 442 - 465, Wrap the
post-download auto-selection in runDownload with error handling around
selectModel, using the existing onActivate pattern as guidance. Ensure a
selectModel persistence failure is caught and does not prevent the subsequent
updateCard idle reset, success toast, or cleanup that clears setupBusy, while
preserving the download-success flow and selected-state behavior.

Comment on lines +1945 to +1958
{onOpenSettings ? (
<button
type="button"
className="rounded-md outline-none focus-visible:ring-3 focus-visible:ring-accent-ring"
onClick={() => onOpenSettings('ai')}
aria-label={strings.session.aiStatus.openSettingsAriaLabel(
strings.session.aiStatus[aiChipStatus]
)}
>
<AiStatusChip status={aiChipStatus} />
</button>
) : (
<AiStatusChip status={aiChipStatus} />
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Tailwind CSS v4 outline-none outline-style none forced-colors behavior change

💡 Result:

In Tailwind CSS v4, the behavior of the outline-none utility was changed to address inconsistencies in how focus rings were handled, particularly concerning accessibility and forced-colors modes [1][2]. The key changes are: 1. Renaming and Splitting Functionality: The utility previously known as outline-none (which applied a 2px invisible outline to maintain accessibility in forced-colors mode) has been renamed to outline-hidden [1][2]. A new utility named outline-none has been introduced that strictly sets outline-style: none [1][3]. 2. Handling of Forced Colors: - outline-hidden is designed to hide the default browser outline in standard modes while preserving an outline when forced-colors mode is active [1][3]. This is intended to ensure that users who rely on high-contrast/forced-colors settings still have a visible focus indicator [2]. - outline-none simply removes the outline entirely, regardless of the forced-colors setting [1][3]. 3. Upgrade Path: If you are upgrading from Tailwind CSS v3 to v4, you should replace occurrences of outline-none with outline-hidden if you wish to maintain the previous behavior (i.e., preserving focus visibility in forced-colors mode) [1][2]. These changes were made to provide more predictable behavior and to resolve the issue where the previous outline-none utility was "hijacking" the CSS property name to apply a specific accessibility implementation that not all users expected [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate files:"
fd -a 'SessionView\.tsx|dialog\.(tsx|ts)$|package\.json$|tailwind\.config\.(js|cjs|mjs|ts)$' . | sed 's#^\./##'

echo
echo "SessionView relevant lines around 1920-1965:"
if [ -f src/features/session/SessionView.tsx ]; then
  sed -n '1920,1965p' src/features/session/SessionView.tsx | cat -n
fi

echo
echo "dialog relevant focus pattern:"
if [ -f src/components/ui/dialog.tsx ]; then
  sed -n '40,85p' src/components/ui/dialog.tsx | cat -n
fi

echo
echo "Search Settings gear button aria-haspopup in SessionView:"
rg -n 'aria-haspopup="dialog"|onOpenSettings|AiStatusChip|outline-none|outline-hidden|ring-3|ring-accent-ring' src/features/session/SessionView.tsx

echo
echo "Tailwind config/package clues:"
for f in package.json tailwind.config.js tailwind.config.cjs tailwind.config.mjs tailwind.config.ts; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,220p' "$f"
  fi
done

Repository: scotej/studyvis

Length of output: 8400


🌐 Web query:

MDN button aria-haspopup dialog opens dialog button dialog attributes

💡 Result:

According to the ARIA specification and official MDN documentation, the aria-haspopup attribute is used to indicate that an interactive element, such as a button, triggers a popup [1][2]. When the triggered popup is a dialog, you set aria-haspopup="dialog" on the button [1][3]. Key considerations for this implementation include: Technical Requirements: 1. Role matching: Ensure the container triggered by the button has the role="dialog" (or is a native element) [1][2]. 2. Focus management: When the dialog opens, keyboard focus must be moved to an appropriate element inside the dialog (e.g., the first focusable control or a close button) [4][5]. When the dialog closes, focus should return to the button that triggered it [4][6]. 3. Labeling: The dialog should be given an accessible name, typically using aria-labelledby to point to the dialog's title [5][6]. Accessibility Context and Debate: While aria-haspopup="dialog" is technically correct according to the ARIA specification [1][7], its practical value and implementation are subjects of ongoing discussion within the accessibility community [8][9][7]: - Variable Support: Historically, some screen readers have not announced "dialog" reliably or have inconsistently treated buttons with this attribute [9][7]. - User Experience: Some experts argue that adding aria-haspopup="dialog" can lead to redundant or confusing announcements for screen reader users, potentially cluttering the experience [8][7]. Others suggest it helps inform users that an action will surface content on the screen [9]. - Recommendations: Because of these inconsistencies, some developers prefer to omit aria-haspopup="dialog" and instead ensure the button's label clearly describes its purpose (e.g., "Manage Settings" instead of "Settings"), or they use other established patterns for dialog accessibility without relying on this specific attribute [7][10]. For robust dialogs, modern development favors the native HTML element, which provides built-in modal functionality (via showModal) and handles focus trapping and the inert state automatically, often reducing the need for manual ARIA management [10].

Citations:


Use outline-hidden, not outline-none, with this focus ring.

This button uses outline-none with focus-visible:ring-3 focus-visible:ring-accent-ring. In Tailwind v4, outline-none removes outlines everywhere, including forced-colors; ring utilities do not provide the required focus indicator in that mode. Use the same pattern as src/components/ui/dialog.tsx, which pairs that ring with focus-visible:outline-hidden.

While here, add aria-haspopup="dialog" to match the adjacent Settings gear icon button because this button opens the same Settings dialog.

Proposed change
           {onOpenSettings ? (
             <button
               type="button"
-              className="rounded-md outline-none focus-visible:ring-3 focus-visible:ring-accent-ring"
+              className="rounded-md outline-hidden focus-visible:ring-3 focus-visible:ring-accent-ring"
               onClick={() => onOpenSettings('ai')}
+              aria-haspopup="dialog"
               aria-label={strings.session.aiStatus.openSettingsAriaLabel(
                 strings.session.aiStatus[aiChipStatus]
               )}
             >
               <AiStatusChip status={aiChipStatus} />
             </button>
           ) : (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{onOpenSettings ? (
<button
type="button"
className="rounded-md outline-none focus-visible:ring-3 focus-visible:ring-accent-ring"
onClick={() => onOpenSettings('ai')}
aria-label={strings.session.aiStatus.openSettingsAriaLabel(
strings.session.aiStatus[aiChipStatus]
)}
>
<AiStatusChip status={aiChipStatus} />
</button>
) : (
<AiStatusChip status={aiChipStatus} />
)}
{onOpenSettings ? (
<button
type="button"
className="rounded-md outline-hidden focus-visible:ring-3 focus-visible:ring-accent-ring"
onClick={() => onOpenSettings('ai')}
aria-haspopup="dialog"
aria-label={strings.session.aiStatus.openSettingsAriaLabel(
strings.session.aiStatus[aiChipStatus]
)}
>
<AiStatusChip status={aiChipStatus} />
</button>
) : (
<AiStatusChip status={aiChipStatus} />
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/session/SessionView.tsx` around lines 1945 - 1958, Update the
settings-opening button in the onOpenSettings branch of SessionView to use
focus-visible:outline-hidden instead of outline-none alongside its existing
focus ring, and add aria-haspopup="dialog" to identify the dialog it opens.
Leave the non-button AiStatusChip rendering unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve Ui regarding benchmarking

2 participants