Skip to content

feat: dark/light theme toggle and live provider model list - #682

Merged
delete-cloud merged 1 commit into
mainfrom
feat/webui-theme-model-list
Jul 25, 2026
Merged

delete-cloud merged 1 commit into
mainfrom
feat/webui-theme-model-list

Conversation

@delete-cloud

@delete-cloud delete-cloud commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

改动

主题切换:light 主题复用同一套 CSS 自定义属性做覆盖,header 加切换按钮,localStorage 持久化,渲染前初始化避免闪烁,默认仍为暗色。

模型列表:新增 GET /providers/{provider}/models(已实测 kimi-code 返回 kimi-for-coding / kimi-for-coding-highspeed / k3 / k3-256k)。前端 model 输入的 datalist 改为拉取实时列表,失败回退内置预设,保留自由输入。

测试

  • 服务端 8 个端点测试(live/422/unavailable/超时/鉴权)
  • vitest 33/33(新增 6 个:主题持久化、live 列表、回退)
  • ruff 干净

Summary by CodeRabbit

  • New Features

    • Added live provider model discovery to the model selector, with preset fallbacks when unavailable.
    • Added a provider model-listing API endpoint with timeout and graceful failure handling.
    • Added a persistent dark/light theme toggle, including saved preferences and light-theme styling.
  • Documentation

    • Documented the provider model-listing endpoint, response format, validation, and fallback behavior.
  • Tests

    • Added coverage for model discovery, authentication, fallback behavior, timeouts, and theme persistence.

Server: new GET /providers/{provider}/models endpoint that lists models
through the provider's OpenAI-compatible client (10s timeout, fresh
short-lived plugin instance per request, closes its client). Failures
(no key, network, non-OpenAI provider) return source=unavailable with an
empty list — never 500. Contract doc updated.

Webui: light theme overrides for the CSS custom properties with a
localStorage-persisted toggle in the header (dark stays default,
pre-render init avoids flash). The model input's datalist now fetches
live model ids for the selected provider (debounced, stale requests
aborted), falling back to the built-in presets when unavailable; input
stays free-text.

Tests: 8 endpoint tests + 33 vitest (6 new); ruff clean.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The API now lists provider models with timeout and fallback behavior, and the WebUI displays live model options while retaining presets. The WebUI also gains persistent dark/light theme selection with corresponding styling and tests.

Changes

Provider model listing

Layer / File(s) Summary
Provider listing contracts and capability
coding-agent/src/coding_agent/server/schemas.py, coding-agent/docs/web-client-api-contract.md, coding-agent/src/coding_agent/providers/openai_compat.py
Defines ProviderModelsResponse and documents the new endpoint; OpenAI-compatible providers retrieve model IDs from /models.
Provider listing server flow
coding-agent/src/coding_agent/server/provider_models.py, coding-agent/src/coding_agent/server/http_server.py
Constructs providers, enforces a 10-second timeout, closes clients, and returns live or unavailable responses through the authenticated endpoint.
Provider listing validation
coding-agent/tests/ui/test_provider_models.py
Tests successful, invalid, unavailable, empty, authenticated, non-compatible, cleanup, and timeout behavior.
WebUI live model selection
coding-agent/webui/app/src/lib/types.ts, coding-agent/webui/app/src/lib/api.ts, coding-agent/webui/app/src/components/Header.tsx
Adds the client response type and API method; the header debounces live model requests, aborts stale requests, and falls back to presets.
WebUI model listing validation
coding-agent/webui/app/src/App.test.tsx
Tests preset models, live updates, free-text input, unavailable responses, failed requests, and server-default behavior.

Persistent WebUI themes

Layer / File(s) Summary
Theme state and initialization
coding-agent/webui/app/src/main.tsx, coding-agent/webui/app/src/App.tsx, coding-agent/webui/app/src/components/Header.tsx
Loads the saved theme before rendering, synchronizes theme state with the document and local storage, and adds a header toggle.
Theme styling and validation
coding-agent/webui/app/src/index.css, coding-agent/webui/app/src/App.test.tsx
Adds light-theme variables and color-scheme handling, with tests for toggling, persistence, defaults, and restoration.

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

Sequence Diagram(s)

sequenceDiagram
  participant WebUI as AgentClient
  participant API as Provider models endpoint
  participant Helper as list_provider_models
  participant Provider as OpenAICompatProvider
  participant Remote as Provider /models API
  WebUI->>API: GET /providers/{provider}/models
  API->>Helper: list_provider_models(provider)
  Helper->>Provider: provide_llm(provider)
  Helper->>Provider: list_models()
  Provider->>Remote: GET /models
  Remote-->>Provider: model records
  Provider-->>Helper: model IDs
  Helper-->>API: model IDs or timeout/error
  API-->>WebUI: live or unavailable response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: theme toggling and live provider model listing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/webui-theme-model-list

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.

@delete-cloud
delete-cloud merged commit 5c421ed into main Jul 25, 2026
1 of 2 checks passed
@delete-cloud
delete-cloud deleted the feat/webui-theme-model-list branch July 25, 2026 04:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
coding-agent/webui/app/src/App.tsx (1)

31-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use one shared theme-storage key.

The same string is maintained in two files; drift could make the pre-rendered theme and React persistence use different keys.

  • coding-agent/webui/app/src/App.tsx#L31-L32: export the key from a shared module.
  • coding-agent/webui/app/src/main.tsx#L8-L9: import and use that shared key instead of the literal.
🤖 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 `@coding-agent/webui/app/src/App.tsx` around lines 31 - 32, The theme-storage
key is duplicated between App.tsx and main.tsx; export THEME_LS_KEY from the
shared module in coding-agent/webui/app/src/App.tsx (lines 31-32), then import
and use that exported key in coding-agent/webui/app/src/main.tsx (lines 8-9)
instead of the literal.
🤖 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 `@coding-agent/src/coding_agent/server/provider_models.py`:
- Around line 35-42: Ensure provider instances created by provide_llm() are
closed even when they are not OpenAICompatProvider: restructure the flow around
the isinstance check so cleanup runs before returning an empty list, while
preserving the existing compatible-provider timeout and close behavior. Update
coding-agent/src/coding_agent/server/provider_models.py lines 35-42 accordingly;
update coding-agent/tests/ui/test_provider_models.py lines 144-151 to use a
non-compatible fake provider and assert that it is closed.

In `@coding-agent/webui/app/src/components/Header.tsx`:
- Around line 49-79: Reset liveModels to null immediately whenever
config.provider changes, before scheduling the debounced listProviderModels
request. Update the useEffect handling provider changes so switching directly
between non-empty providers falls back to MODEL_PRESETS during the debounce and
fetch period, while preserving the existing abort and response handling.

---

Nitpick comments:
In `@coding-agent/webui/app/src/App.tsx`:
- Around line 31-32: The theme-storage key is duplicated between App.tsx and
main.tsx; export THEME_LS_KEY from the shared module in
coding-agent/webui/app/src/App.tsx (lines 31-32), then import and use that
exported key in coding-agent/webui/app/src/main.tsx (lines 8-9) instead of the
literal.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8902b8cd-90ae-4c65-a3e8-55c9357ae0be

📥 Commits

Reviewing files that changed from the base of the PR and between e601f26 and 3773623.

📒 Files selected for processing (13)
  • coding-agent/docs/web-client-api-contract.md
  • coding-agent/src/coding_agent/providers/openai_compat.py
  • coding-agent/src/coding_agent/server/http_server.py
  • coding-agent/src/coding_agent/server/provider_models.py
  • coding-agent/src/coding_agent/server/schemas.py
  • coding-agent/tests/ui/test_provider_models.py
  • coding-agent/webui/app/src/App.test.tsx
  • coding-agent/webui/app/src/App.tsx
  • coding-agent/webui/app/src/components/Header.tsx
  • coding-agent/webui/app/src/index.css
  • coding-agent/webui/app/src/lib/api.ts
  • coding-agent/webui/app/src/lib/types.ts
  • coding-agent/webui/app/src/main.tsx

Comment on lines +35 to +42
plugin = LLMProviderPlugin(provider=provider, model="", api_key="")
instance = plugin.provide_llm()
if not isinstance(instance, OpenAICompatProvider):
return []
try:
return await asyncio.wait_for(instance.list_models(), timeout=timeout)
finally:
await instance.close()

Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'coding-agent/(src|tests)/.*provider_models' || true

echo "== provider_models.py outline/section =="
if [ -f coding-agent/src/coding_agent/server/provider_models.py ]; then
  wc -l coding-agent/src/coding_agent/server/provider_models.py
  cat -n coding-agent/src/coding_agent/server/provider_models.py
fi

echo "== test section =="
if [ -f coding-agent/tests/ui/test_provider_models.py ]; then
  wc -l coding-agent/tests/ui/test_provider_models.py
  sed -n '120,175p' coding-agent/tests/ui/test_provider_models.py | cat -n
fi

echo "== related provider plugin/class definitions =="
rg -n "class LLMProviderPlugin|provide_llm|class OpenAICompatProvider|close\\(" coding-agent -S || true

Repository: delete-cloud/Coding-Agent-Loop

Length of output: 36514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate llm provider and compatibility base/provider implementations =="
git ls-files 'coding-agent/src/coding_agent/plugins/llm_provider.py' 'coding-agent/src/coding_agent/providers/*.py' | xargs -r rg -n 'class .*Provider|class .*Compat|def close|provide_llm|plugin_manager'

echo "== llm_provider implementation =="
cat -n coding-agent/src/coding_agent/plugins/llm_provider.py

echo "== provider implementations with close =="
for f in coding-agent/src/coding_agent/providers/*.py; do
  if rg -q 'class .*Provider|async def close|def close' "$f"; then
    echo "--- $f ---"
    wc -l "$f"
    rg -n -C 4 'class .*Provider|async def close|def close|def __init__|http_client' "$f" | sed -n '1,220p' || true
  fi
done

Repository: delete-cloud/Coding-Agent-Loop

Length of output: 15831


Close non-compatible provider instances before returning an empty list. provide_llm() constructs instances before isinstance(instance, OpenAICompatProvider) checks them, so non-compatible providers bypass finally and keep their short-lived cleanup resources alive.

  • coding-agent/src/coding_agent/server/provider_models.py#L35-L42: return the non-compatible provider to cleanup instead of returning early, or skip construction via an explicit OpenAI-compatible provider-only path.
  • coding-agent/tests/ui/test_provider_models.py#L144-L151: replace the sentinel with a non-compatible provider/fake and assert it is closed.
📍 Affects 2 files
  • coding-agent/src/coding_agent/server/provider_models.py#L35-L42 (this comment)
  • coding-agent/tests/ui/test_provider_models.py#L144-L151
🤖 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 `@coding-agent/src/coding_agent/server/provider_models.py` around lines 35 -
42, Ensure provider instances created by provide_llm() are closed even when they
are not OpenAICompatProvider: restructure the flow around the isinstance check
so cleanup runs before returning an empty list, while preserving the existing
compatible-provider timeout and close behavior. Update
coding-agent/src/coding_agent/server/provider_models.py lines 35-42 accordingly;
update coding-agent/tests/ui/test_provider_models.py lines 144-151 to use a
non-compatible fake provider and assert that it is closed.

Comment on lines +49 to +79
client,
theme,
onToggleTheme,
}: Props) {
// Live model ids for the selected provider; null = use MODEL_PRESETS.
const [liveModels, setLiveModels] = useState<string[] | null>(null);

useEffect(() => {
const provider = config.provider.trim();
if (!provider) {
setLiveModels(null);
return;
}
const ctrl = new AbortController();
// Debounce rapid provider switches; the abort drops superseded requests.
const timer = setTimeout(() => {
client
.listProviderModels(provider, ctrl.signal)
.then((res) => {
setLiveModels(res.source === "live" && res.models.length > 0 ? res.models : null);
})
.catch(() => setLiveModels(null));
}, 250);
return () => {
clearTimeout(timer);
ctrl.abort();
};
}, [client, config.provider]);

const modelOptions = liveModels ?? MODEL_PRESETS;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stale model list shown briefly when switching providers.

liveModels is only reset to null when the provider is cleared (Line 59) or after the new fetch resolves/rejects (Lines 68-70). When switching directly from one non-empty provider to another, the previous provider's live model ids remain in modelOptions for the debounce delay + fetch duration, so the datalist can momentarily offer model ids that don't belong to the newly selected provider.

🐛 Proposed fix
   useEffect(() => {
     const provider = config.provider.trim();
     if (!provider) {
       setLiveModels(null);
       return;
     }
+    // Avoid showing the previous provider's (now mismatched) model list
+    // while the new provider's fetch is in flight.
+    setLiveModels(null);
     const ctrl = new AbortController();
📝 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
client,
theme,
onToggleTheme,
}: Props) {
// Live model ids for the selected provider; null = use MODEL_PRESETS.
const [liveModels, setLiveModels] = useState<string[] | null>(null);
useEffect(() => {
const provider = config.provider.trim();
if (!provider) {
setLiveModels(null);
return;
}
const ctrl = new AbortController();
// Debounce rapid provider switches; the abort drops superseded requests.
const timer = setTimeout(() => {
client
.listProviderModels(provider, ctrl.signal)
.then((res) => {
setLiveModels(res.source === "live" && res.models.length > 0 ? res.models : null);
})
.catch(() => setLiveModels(null));
}, 250);
return () => {
clearTimeout(timer);
ctrl.abort();
};
}, [client, config.provider]);
const modelOptions = liveModels ?? MODEL_PRESETS;
client,
theme,
onToggleTheme,
}: Props) {
// Live model ids for the selected provider; null = use MODEL_PRESETS.
const [liveModels, setLiveModels] = useState<string[] | null>(null);
useEffect(() => {
const provider = config.provider.trim();
if (!provider) {
setLiveModels(null);
return;
}
// Avoid showing the previous provider's (now mismatched) model list
// while the new provider's fetch is in flight.
setLiveModels(null);
const ctrl = new AbortController();
// Debounce rapid provider switches; the abort drops superseded requests.
const timer = setTimeout(() => {
client
.listProviderModels(provider, ctrl.signal)
.then((res) => {
setLiveModels(res.source === "live" && res.models.length > 0 ? res.models : null);
})
.catch(() => setLiveModels(null));
}, 250);
return () => {
clearTimeout(timer);
ctrl.abort();
};
}, [client, config.provider]);
const modelOptions = liveModels ?? MODEL_PRESETS;
🤖 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 `@coding-agent/webui/app/src/components/Header.tsx` around lines 49 - 79, Reset
liveModels to null immediately whenever config.provider changes, before
scheduling the debounced listProviderModels request. Update the useEffect
handling provider changes so switching directly between non-empty providers
falls back to MODEL_PRESETS during the debounce and fetch period, while
preserving the existing abort and response handling.

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.

1 participant