Skip to content

feat(webui): connection profiles with status-driven header - #689

Merged
delete-cloud merged 1 commit into
mainfrom
feat/webui-connection-profiles
Aug 1, 2026
Merged

delete-cloud merged 1 commit into
mainfrom
feat/webui-connection-profiles

Conversation

@delete-cloud

@delete-cloud delete-cloud commented Aug 1, 2026 •

Copy link
Copy Markdown
Owner

Header 的 URL/key 裸输入框 → 连接指示器(状态点+档案名+版本号)+ 连接档案面板:

  • 多档案(localStorage 存储,自动迁移旧 config),一键切换并完整清理会话状态
  • 测试连接门槛:/healthz 可达性 + /sessions 鉴权双探针,通过才能保存(可 save anyway)
  • 切换后端有 sessionsSeqRef 防旧数据串台

评审一轮:1 P1 + 2 P2 已修;128 vitest 全绿,tsc 干净

Summary by CodeRabbit

  • New Features
    • Added connection profiles for saving, switching, editing, adding, and deleting backend connections.
    • Added connection testing with health status, API-key validation, server version details, and failure feedback.
    • Added automatic profile persistence and migration of existing connection settings.
    • Added safeguards to prevent outdated session data from replacing results from the active connection.
  • Bug Fixes
    • Improved connection and session refresh behavior when switching backends.
    • Prevented stale health and session requests from updating the interface.

Replace the always-visible base URL / API key inputs with a connection
indicator (status dot + profile name + server version) and a
ConnectionPanel: multiple saved profiles (localStorage, migrated from
the legacy config), one-click switch with full session-state cleanup,
and a test-connection gate that validates both reachability (/healthz)
and the API key (authenticated /sessions probe) before saving.

Review round: 1 P1 (stale session list from old backend after profile
switch — sessionsSeqRef guard) + 2 P2 (unauthenticated healthz gave
false-green tests; in-flight test results unlocked Save for edited
values) all fixed. 128 vitest, tsc clean.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Web UI now supports persisted connection profiles. Users can switch, add, edit, test, and delete profiles. Header health checks use abort signals. App session loading resets and rejects stale responses when the active backend changes.

Changes

Connection profile management

Layer / File(s) Summary
Profile storage and CRUD
coding-agent/webui/app/src/lib/profiles.ts, coding-agent/webui/app/src/lib/profiles.test.ts
Adds profile contracts, localStorage persistence, legacy migration, sanitization, CRUD operations, active-profile handling, and storage tests.
Profile management panel
coding-agent/webui/app/src/components/ConnectionPanel.tsx, coding-agent/webui/app/src/components/ConnectionPanel.test.tsx
Adds profile listing, editing, creation, deletion safeguards, connection tests, API-key validation, save gating, and stale-result handling.
Header connection control
coding-agent/webui/app/src/components/Header.tsx, coding-agent/webui/app/src/components/Header.test.tsx, coding-agent/webui/app/src/lib/api.ts
Replaces inline connection inputs with a profile control and health indicator. Health requests support abort signals and display server status and version.
Application client and session integration
coding-agent/webui/app/src/App.tsx, coding-agent/webui/app/src/App.test.tsx
Applies the active profile to the runtime client, resets session state on profile changes, refetches sessions after client changes, and ignores stale session responses.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Header
  participant ConnectionPanel
  participant App
  participant Backend

  User->>Header: open connection control
  Header->>ConnectionPanel: display profile store
  User->>ConnectionPanel: test or select profile
  ConnectionPanel->>Backend: check health and authenticated sessions
  Backend-->>ConnectionPanel: return connection result
  ConnectionPanel->>App: submit profile store change
  App->>Backend: reload session list
  Backend-->>App: return sessions
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 main change: adding connection profiles and a status-driven header to the web UI.
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 💡 1
📝 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-connection-profiles

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.

@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.

🧹 Nitpick comments (7)
coding-agent/webui/app/src/components/ConnectionPanel.tsx (3)

260-280: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add accessible names to the form inputs.

The three inputs carry only placeholder and, for two of them, title. The API key input has neither title nor a label. Screen readers announce the placeholder inconsistently across browsers, and the placeholder disappears once the user types. Add aria-label to each input.

♿ Proposed labels
           <input
             className={inputCls}
             type="password"
             placeholder="X-API-Key"
+            aria-label="API key"
             value={apiKey}
             onChange={(e) => onFieldChange(setApiKey)(e.target.value)}
           />

Note: ConnectionPanel.test.tsx line 85 and Header.test.tsx line 184 query the API key input by placeholder text, so keep the placeholder attribute.

🤖 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/ConnectionPanel.tsx` around lines 260 -
280, Update the three inputs in the ConnectionPanel form to include explicit
aria-label values describing the profile name, server base URL, and API key
fields. Preserve the existing placeholder attributes, values, and change
handlers so current behavior and placeholder-based tests remain unchanged.

139-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider allowing metadata-only edits without a connection test.

canSave requires test.state === "ok" or a failed test plus saveAnyway. In edit mode a user who only renames a profile must run a network test against the server. If that server is currently offline, the user must also select "save anyway", which suggests the profile is broken.

One option: allow save without a test when mode.kind === "edit" and neither baseUrl nor apiKey changed from the stored profile.

🤖 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/ConnectionPanel.tsx` around lines 139 -
142, The canSave condition in ConnectionPanel should allow edit-mode
metadata-only saves when both baseUrl and apiKey remain unchanged from the
stored profile, without requiring a successful connection test or saveAnyway.
Preserve the existing validation and test requirements for new profiles and
edits that change connection settings.

109-137: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout and abort to the connection test.

testConnection calls client.health() and client.listSessions() with no AbortSignal and no timeout. If the target host does not answer (for example a wrong port that drops packets), the fetch can stay pending for the browser default period. The button stays disabled and shows "Testing…" with no way to cancel. The pending promise also survives panel close, because nothing aborts it.

AgentClient.health(signal?) already accepts a signal (coding-agent/webui/app/src/lib/api.ts lines 60-61). listSessions() does not, so a full fix needs either a signal parameter there or AbortSignal.timeout wired through a client that supports it.

♻️ Proposed timeout for the health probe
   const testConnection = async () => {
     setTest({ state: "testing" });
     const started = performance.now();
     const snapshot: TestSnapshot = { baseUrl: baseUrl.trim(), apiKey };
     const client = new AgentClient({ baseUrl: snapshot.baseUrl, apiKey: snapshot.apiKey });
+    const ctrl = new AbortController();
+    const timer = setTimeout(() => ctrl.abort(), 10_000);
     try {
-      const h = await client.health();
+      const h = await client.health(ctrl.signal);

Clear timer in a finally block, and extend listSessions to accept a signal so the authenticated probe is bounded too.

🤖 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/ConnectionPanel.tsx` around lines 109 -
137, Update testConnection to use an AbortController with a timeout covering
both client.health and client.listSessions, and abort the request when the panel
unmounts or the timeout expires. Extend AgentClient.listSessions to accept and
forward an optional AbortSignal, clear the timeout in a finally block, and
preserve the existing snapshot checks and error reporting.
coding-agent/webui/app/src/components/Header.tsx (2)

168-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the ARIA pattern between the trigger and the panel.

The trigger uses aria-expanded, which describes a disclosure widget. ConnectionPanel uses role="dialog" (coding-agent/webui/app/src/components/ConnectionPanel.tsx line 178) without aria-modal, focus move on open, or focus return on close. Screen reader users receive mixed signals about the widget type.

Pick one pattern. For a dropdown, drop role="dialog" and keep aria-expanded with aria-controls pointing at the panel id. For a dialog, move focus into the panel on open and restore focus to the trigger on close.

🤖 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 168 - 190, Use
the dropdown pattern for the connection control: update the trigger in Header’s
connection panel block to include aria-controls referencing a stable panel id,
and update ConnectionPanel to remove role="dialog" while retaining the existing
aria-expanded behavior. Ensure the panel uses the same id so the trigger and
controlled content are consistently associated.

83-104: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The health state can stay stale until the client changes.

The effect depends only on client. It runs once per client identity. If the server stops responding after a successful check, the indicator keeps showing green and the previous version. Users read this control as live connection status.

Consider a periodic re-check, for example a 30-second interval inside the same effect, with the existing abort logic reused for cleanup. Alternatively re-check when the window regains focus.

🤖 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 83 - 104, The
health-check effect keyed by client identity does not refresh after a successful
check, leaving the indicator stale. Update the effect containing the health
request to periodically re-run the check, such as every 30 seconds, while
reusing the existing AbortController and cleanup logic so timers and in-flight
requests are canceled when the effect is disposed.
coding-agent/webui/app/src/lib/profiles.ts (1)

60-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Trim baseUrl and name in sanitizeProfile.

sanitizeProfile tests p.baseUrl.trim() but stores the raw value. A persisted profile with surrounding whitespace stays untrimmed, and AgentClient.url() only strips trailing slashes. addProfile trims, so the two entry points differ.

♻️ Proposed normalization
   return {
     id: typeof p.id === "string" && p.id ? p.id : generateId(),
-    name: typeof p.name === "string" && p.name.trim() ? p.name : deriveName(p.baseUrl),
-    baseUrl: p.baseUrl,
+    name: typeof p.name === "string" && p.name.trim() ? p.name.trim() : deriveName(p.baseUrl),
+    baseUrl: p.baseUrl.trim(),
     apiKey: typeof p.apiKey === "string" ? p.apiKey : "",
   };

Note: profiles.test.ts line 73 asserts store.profiles equals the stored objects verbatim, so that expectation stays valid only while fixtures contain no whitespace.

🤖 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/lib/profiles.ts` around lines 60 - 69, Update
sanitizeProfile to store a trimmed baseUrl after validating it, and trim valid
name values before returning the normalized profile. Preserve deriveName
behavior for missing or blank names, and keep addProfile and persisted-profile
loading consistent through the same normalization.
coding-agent/webui/app/src/App.tsx (1)

66-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

loadProfiles() runs twice at startup.

Line 75 calls loadProfiles() inside loadConfig, and line 108 calls it again as the useState initializer. Each call reads and parses localStorage, and the first call may seed and persist a new store.

If the seed write fails, saveProfiles swallows the error (coding-agent/webui/app/src/lib/profiles.ts lines 131-135). The second call then seeds again and produces a different id through generateId(). config.baseUrl and config.apiKey still match, because both seeds derive from the same legacy config or defaultBaseUrl(), so handleProfilesChange behaves correctly. The duplicated work and the divergent-id path are still avoidable.

Load the store once and derive both states from it.

♻️ Proposed single load
-function loadConfig(): Config {
+function loadConfig(store: ProfileStore): Config {
   const defaults: Config = {
     baseUrl: defaultBaseUrl(),
@@
   // The active connection profile owns baseUrl/apiKey; the legacy config's
   // copies are only migration input for the profiles store.
-  const active = activeProfile(loadProfiles());
+  const active = activeProfile(store);
   if (active) {

Then inside App:

-  const [config, setConfig] = useState<Config>(loadConfig);
-  const [profiles, setProfiles] = useState<ProfileStore>(loadProfiles);
+  const [profiles, setProfiles] = useState<ProfileStore>(loadProfiles);
+  const [config, setConfig] = useState<Config>(() => loadConfig(profiles));

Note: a useState initializer cannot read another state value declared in the same render in that form. Use a single useMemo-free helper that returns both values, or compute the store in a module-level lazy initializer shared by both useState calls.

Also applies to: 108-108

🤖 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 66 - 80, Update the startup
initialization around loadConfig and the App useState initializer so
loadProfiles executes only once and both the initial config and profiles state
derive from that same loaded store. Use a shared helper returning both values,
or an equivalent module-level lazy initializer, while preserving activeProfile’s
baseUrl/apiKey override and avoiding repeated seeding or parsing.
🤖 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.

Nitpick comments:
In `@coding-agent/webui/app/src/App.tsx`:
- Around line 66-80: Update the startup initialization around loadConfig and the
App useState initializer so loadProfiles executes only once and both the initial
config and profiles state derive from that same loaded store. Use a shared
helper returning both values, or an equivalent module-level lazy initializer,
while preserving activeProfile’s baseUrl/apiKey override and avoiding repeated
seeding or parsing.

In `@coding-agent/webui/app/src/components/ConnectionPanel.tsx`:
- Around line 260-280: Update the three inputs in the ConnectionPanel form to
include explicit aria-label values describing the profile name, server base URL,
and API key fields. Preserve the existing placeholder attributes, values, and
change handlers so current behavior and placeholder-based tests remain
unchanged.
- Around line 139-142: The canSave condition in ConnectionPanel should allow
edit-mode metadata-only saves when both baseUrl and apiKey remain unchanged from
the stored profile, without requiring a successful connection test or
saveAnyway. Preserve the existing validation and test requirements for new
profiles and edits that change connection settings.
- Around line 109-137: Update testConnection to use an AbortController with a
timeout covering both client.health and client.listSessions, and abort the
request when the panel unmounts or the timeout expires. Extend
AgentClient.listSessions to accept and forward an optional AbortSignal, clear
the timeout in a finally block, and preserve the existing snapshot checks and
error reporting.

In `@coding-agent/webui/app/src/components/Header.tsx`:
- Around line 168-190: Use the dropdown pattern for the connection control:
update the trigger in Header’s connection panel block to include aria-controls
referencing a stable panel id, and update ConnectionPanel to remove
role="dialog" while retaining the existing aria-expanded behavior. Ensure the
panel uses the same id so the trigger and controlled content are consistently
associated.
- Around line 83-104: The health-check effect keyed by client identity does not
refresh after a successful check, leaving the indicator stale. Update the effect
containing the health request to periodically re-run the check, such as every 30
seconds, while reusing the existing AbortController and cleanup logic so timers
and in-flight requests are canceled when the effect is disposed.

In `@coding-agent/webui/app/src/lib/profiles.ts`:
- Around line 60-69: Update sanitizeProfile to store a trimmed baseUrl after
validating it, and trim valid name values before returning the normalized
profile. Preserve deriveName behavior for missing or blank names, and keep
addProfile and persisted-profile loading consistent through the same
normalization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 017bdfd2-fa59-4ad6-92ed-520f83c43f5c

📥 Commits

Reviewing files that changed from the base of the PR and between ecbf542 and b31e7b7.

📒 Files selected for processing (9)
  • coding-agent/webui/app/src/App.test.tsx
  • coding-agent/webui/app/src/App.tsx
  • coding-agent/webui/app/src/components/ConnectionPanel.test.tsx
  • coding-agent/webui/app/src/components/ConnectionPanel.tsx
  • coding-agent/webui/app/src/components/Header.test.tsx
  • coding-agent/webui/app/src/components/Header.tsx
  • coding-agent/webui/app/src/lib/api.ts
  • coding-agent/webui/app/src/lib/profiles.test.ts
  • coding-agent/webui/app/src/lib/profiles.ts

@delete-cloud
delete-cloud merged commit 5bf929b into main Aug 1, 2026
2 checks passed
@delete-cloud
delete-cloud deleted the feat/webui-connection-profiles branch August 1, 2026 18:00
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