Skip to content

Add a config editor page#268

Closed
mcowger wants to merge 3 commits intomostlygeek:mainfrom
mcowger:mcowger/config-editor
Closed

Add a config editor page#268
mcowger wants to merge 3 commits intomostlygeek:mainfrom
mcowger:mcowger/config-editor

Conversation

@mcowger
Copy link
Copy Markdown

@mcowger mcowger commented Aug 25, 2025

Adds a page to live edit the config.yaml :).

Summary by CodeRabbit

  • New Features

    • Added an in-app Config page to view and edit the YAML configuration with load/save, validation, unsaved-change indicator, status messages, font-size and wrap controls, and accessible editor.
    • New server endpoints to GET and PUT the config (/api/config).
  • Reliability

    • Safer atomic saves that preserve file permissions and reduce corruption risk.
    • More consistent admin-controlled reload behavior after config updates.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 25, 2025

Walkthrough

Adds admin wiring and a reload callback to ProxyManager, exposes GET/PUT /api/config to view and atomically update YAML config, introduces atomic file-write utility, and a UI ConfigEditor page with route; server startup now attaches admin controls to new ProxyManager instances.

Changes

Cohort / File(s) Summary of changes
Reloader wiring & admin controls
llama-swap.go, proxy/proxymanager.go
Refactors reload flow to construct a new ProxyManager, call SetAdminControls(configPath, watchConfig, reloadCallback), and swap srv.Handler to the new manager. Adds configPath, reloadCallback, watchConfigEnabled fields and SetAdminControls method on ProxyManager.
Config API registration & handlers
proxy/proxymanager_api.go, proxy/proxymanager_config_api.go
Registers GET /api/config and PUT /api/config. Implements handlers to read current YAML, validate incoming YAML, write atomically preserving file mode, and trigger reload logic: rely on fsnotify watcher when enabled or emit ConfigFileChangedEvent and call reload callback when not watching.
Atomic file write utility
proxy/fileutil.go
Adds WriteFileAtomic(path, data, mode) performing temp-file write, fsync file and directory, preserve existing file mode when present, atomic rename, and cleanup on failure.
UI routing and Config editor
ui/src/App.tsx, ui/src/pages/ConfigEditor.tsx
Adds Config nav item and /config route; introduces ConfigEditor component that GETs and PUTs /api/config, supports editing with wrap/font-size prefs, save with status feedback, re-fetch on reconnect when clean, and robust error handling. Minor typing tweaks in App.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CE as UI: ConfigEditor
  participant S as Server: ProxyManager
  participant FS as File System
  participant EB as Event Bus
  participant W as fsnotify Watcher

  rect rgb(243,248,255)
    note right of CE: Load config
    U->>CE: Open /config
    CE->>S: GET /api/config
    S->>FS: Read pm.configPath
    FS-->>S: YAML bytes or error
    S-->>CE: 200 text/plain (YAML) or 500
  end

  rect rgb(241,252,244)
    note right of CE: Save edited config
    U->>CE: Click Save
    CE->>S: PUT /api/config (text/plain body)
    S->>S: Validate YAML (LoadConfigFromReader)
    alt valid
      S->>FS: WriteFileAtomic(pm.configPath, data, mode)
      FS-->>S: ok
      alt watchConfigEnabled == true
        note over S,W: Rely on watcher to detect change
        S-->>CE: 200 {"msg":"ok"}
        FS-->>W: file change event
        W->>S: change detected
        S->>EB: ConfigFileChangedEvent (start)
      else watchConfigEnabled == false
        S->>EB: ConfigFileChangedEvent (start)
        S->>S: reloadCallback() (if non-nil)
        S-->>CE: 200 {"msg":"ok"}
      end
    else invalid or write error
      S-->>CE: 400/500 with error
    end
  end
Loading
sequenceDiagram
  autonumber
  participant RS as Reloader (llama-swap.go)
  participant PM as ProxyManager (newPM)
  participant SRV as HTTP Server

  note over RS: Reload or initial start
  RS->>PM: proxy.New(config)
  RS->>PM: SetAdminControls(configPath, watchConfig, reloadProxyManager)
  alt replacing existing handler
    RS->>SRV: srv.Handler = PM
  else initial start
    RS->>SRV: srv.Handler = PM
  end
  RS->>RS: Emit ConfigFileChangedEvent after delay (unchanged)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • mostlygeek
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
llama-swap.go (2)

68-82: Avoid data race: replacing srv.Handler at runtime is unsafe; use an atomic handler switcher.

http.Server reads Handler concurrently on each request. Writing srv.Handler from reloadProxyManager causes a data race and may serve a partially torn handler under load. Replace the direct assignment with an always-present delegating handler that atomically swaps the inner handler.

Apply this diff within the changed block to stop assigning to srv.Handler directly:

-            srv.Handler = newPM
+            // swap through atomic switcher instead of mutating http.Server
+            handlerSwitcher.Store(newPM)

And similarly below:

-            srv.Handler = newPM
+            handlerSwitcher.Store(newPM)

Outside this hunk (supporting code), introduce an atomic delegating handler and initialize http.Server once with it:

// supporting code (outside selected lines)
type atomicHandler struct{ v atomic.Value } // stores http.Handler
func (h *atomicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  if hh, ok := h.v.Load().(http.Handler); ok && hh != nil {
    hh.ServeHTTP(w, r)
    return
  }
  http.NotFound(w, r)
}
func (h *atomicHandler) Store(next http.Handler) { h.v.Store(next) }

// after creating srv := &http.Server{ Addr: *listenStr }, set:
var handlerSwitcher atomicHandler
srv.Handler = &handlerSwitcher

// on first load (else branch) also call:
// handlerSwitcher.Store(newPM)

This removes the race while allowing hot-swaps without restarting the server. If you prefer, sync/atomic.Value can be replaced with an RWMutex-guarded pointer. Also consider postponing Shutdown of the old manager until the switch is complete (see next comment).


68-82: Serialize concurrent reloads to prevent overlapping Shutdown/New cycles.

reloadProxyManager can be called from fsnotify (debounced) and from the config PUT callback. Without a guard, two reloads can interleave, racing on internal ProxyManager state and the handler swap.

Add a reload mutex:

@@
-  var reloadProxyManager func()
+  var reloadProxyManager func()
+  var reloadMu sync.Mutex
   reloadProxyManager = func() {
+    reloadMu.Lock()
+    defer reloadMu.Unlock()

Supporting change: add sync to imports.

🧹 Nitpick comments (15)
proxy/proxymanager.go (1)

631-637: Make SetAdminControls thread-safe (future-proof), and document call timing.

Even if today this is only called on a fresh ProxyManager before serving, protecting against future races is cheap. Wrap the assignments with the manager’s mutex.

 func (pm *ProxyManager) SetAdminControls(configPath string, watchEnabled bool, reload func()) {
-    pm.configPath = configPath
-    pm.watchConfigEnabled = watchEnabled
-    pm.reloadCallback = reload
+    pm.Lock()
+    defer pm.Unlock()
+    pm.configPath = configPath
+    pm.watchConfigEnabled = watchEnabled
+    pm.reloadCallback = reload
 }

Also add a short doc comment noting it should be called before starting the HTTP server (or that it’s safe to call at runtime).

proxy/fileutil.go (1)

42-47: Redundant cleanup paths; simplify to one well-scoped defer.

You both define cleanup() and also defer a second best‑effort os.Remove(tmpName). It’s harmless but noisy. Consider a single deferred cleanup that closes/removes the temp file only if rename hasn’t succeeded.

- tmpName := tmpFile.Name()
- cleanup := func() {
-   _ = tmpFile.Close()
-   _ = os.Remove(tmpName)
- }
-
- // Ensure cleanup on any failure path
- defer func() {
-   // If tmpFile still exists (e.g., rename failed), best-effort remove it
-   _ = os.Remove(tmpName)
- }()
+ tmpName := tmpFile.Name()
+ renamed := false
+ defer func() {
+   // Best-effort close; ignore error if already closed
+   _ = tmpFile.Close()
+   if !renamed {
+     _ = os.Remove(tmpName)
+   }
+ }()

And then after a successful os.Rename, set renamed = true.

- if err = os.Rename(tmpName, path); err != nil {
+ if err = os.Rename(tmpName, path); err != nil {
     // best-effort cleanup
-   _ = os.Remove(tmpName)
    return err
 }
+renamed = true
ui/src/App.tsx (2)

38-46: Drop the unnecessary cast; HTMLHeadingElement already has blur().

e.currentTarget is HTMLHeadingElement, which extends HTMLElement and exposes blur(); the cast is redundant.

-                    (e.currentTarget as HTMLElement).blur();
+                    e.currentTarget.blur();

35-41: Minor a11y polish for editable title.

For a contentEditable heading, add an accessible label to clarify it’s editable.

-              <h1
+              <h1
                 contentEditable
                 suppressContentEditableWarning
                 className="flex items-center p-0 outline-none hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-1"
+                aria-label="Application title (editable)"
+                title="Click to edit the window title"
proxy/proxymanager_config_api.go (5)

15-21: Handle disabled/unknown config path and return YAML content type.

When pm.configPath is empty or the file is missing, returning 404 (or a clear 503) avoids generic 500s. Also set a YAML content type.

 func (pm *ProxyManager) apiGetConfig(c *gin.Context) {
-  data, err := os.ReadFile(pm.configPath)
+  if pm.configPath == "" {
+    c.JSON(http.StatusNotFound, gin.H{"error": "config API disabled"})
+    return
+  }
+  data, err := os.ReadFile(pm.configPath)
   if err != nil {
-    c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+    if os.IsNotExist(err) {
+      c.JSON(http.StatusNotFound, gin.H{"error": "config file not found"})
+    } else {
+      c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+    }
     return
   }
-  c.Data(http.StatusOK, "text/plain; charset=utf-8", data)
+  c.Data(http.StatusOK, "application/x-yaml; charset=utf-8", data)
 }

26-32: Bound request body size to avoid unbounded allocs.

Wrap the body with http.MaxBytesReader (e.g., 1 MiB) before io.ReadAll.

 func (pm *ProxyManager) apiPutConfig(c *gin.Context) {
-  // Read entire body as text (accept text/plain or application/x-yaml, but don't hard fail on content-type)
-  body, err := io.ReadAll(c.Request.Body)
+  // Limit body to 1 MiB to prevent abuse.
+  c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20)
+  // Read entire body as text (accept text/plain or application/x-yaml, but don't hard fail on content-type)
+  body, err := io.ReadAll(c.Request.Body)
   if err != nil {
     c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"})
     return
   }

46-49: Return write error details for easier troubleshooting.

Including err.Error() helps operators diagnose permission/path issues.

-  if err := WriteFileAtomic(pm.configPath, body, mode); err != nil {
-    c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write config file"})
+  if err := WriteFileAtomic(pm.configPath, body, mode); err != nil {
+    c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write config file", "details": err.Error()})
     return
   }

51-62: Don’t block the HTTP response while reloading; fire reload asynchronously.

Reloads can be slow. Emit the event and return quickly; let SSE convey progress.

   } else {
     // Emit start event then call reload callback
     event.Emit(ConfigFileChangedEvent{
       ReloadingState: ReloadingStateStart,
     })
     if pm.reloadCallback != nil {
-      pm.reloadCallback()
+      go pm.reloadCallback()
     }
   }

23-65: Security: Pair route-level same-origin guard with handler checks (defense-in-depth).

If you don’t adopt the middleware approach, minimally verify same-origin here to prevent CSRF against the PUT endpoint. I can fold an inline check into both handlers.

Would you like me to push an alternative patch that performs an Origin/Host check directly inside these handlers instead of route middleware?

llama-swap.go (1)

79-81: Graceful swap order and in-flight requests.

You Shutdown the current manager before constructing and swapping to the new one. If Shutdown cancels request contexts, in-flight requests may fail during a reload. Prefer: build newPM, publish it via handlerSwitcher.Store(newPM), then gracefully drain and Shutdown the old manager.

Example within this block (assuming the atomic switcher from the earlier comment):

- currentPM.Shutdown()
- newPM := proxy.New(config)
- newPM.SetAdminControls(*configPath, *watchConfig, reloadProxyManager)
- handlerSwitcher.Store(newPM)
+ newPM := proxy.New(config)
+ newPM.SetAdminControls(*configPath, *watchConfig, reloadProxyManager)
+ // publish new handler first
+ handlerSwitcher.Store(newPM)
+ // now drain old PM
+ currentPM.Shutdown()

This minimizes disruption during reloads.

ui/src/pages/ConfigEditor.tsx (5)

119-126: Avoid re-fetch during an active save.

The reconnect-triggered fetch can race with a PUT, overwriting the editor with server content mid-save. Gate on saving as well.

-  if (prev !== "connected" && connectionStatus === "connected" && !isDirty) {
+  if (prev !== "connected" && connectionStatus === "connected" && !isDirty && !saving) {

Also add saving to the dependency list of this effect.


167-180: Improve icon button accessibility with aria-labels.

Icon-only buttons need accessible names. Titles aren’t a reliable substitute.

-            <button className="btn" onClick={toggleFontSize} title="Font size">
+            <button className="btn" onClick={toggleFontSize} title="Font size" aria-label="Toggle font size">
               <RiFontSize />
             </button>
-            <button className="btn" onClick={toggleWrap} title={wrapText ? "Disable wrap" : "Enable wrap"}>
+            <button
+              className="btn"
+              onClick={toggleWrap}
+              title={wrapText ? "Disable wrap" : "Enable wrap"}
+              aria-label={wrapText ? "Disable wrap" : "Enable wrap"}
+            >
               {wrapText ? <RiTextWrap /> : <RiAlignJustify />}
             </button>
             <button
               className="btn"
               onClick={handleSave}
               disabled={!isDirty || saving || loading}
-              title="Save configuration"
+              title="Save configuration"
+              aria-label="Save configuration"
             >
               {saving ? "Saving..." : "Save"}
             </button>

195-203: Add keyboard shortcut (Ctrl/Cmd+S) to save.

Small UX win for an editor.

         <textarea
           className={`w-full h-full font-mono ${fontSizeClass} bg-background text-foreground border border-border rounded p-3 outline-none focus:border-primary focus:ring-0 resize-none`}
           value={content}
           onChange={(e) => setContent(e.target.value)}
           spellCheck={false}
           wrap={wrapText ? "soft" : "off"}
           disabled={loading}
           aria-label="YAML configuration editor"
+          onKeyDown={(e) => {
+            if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
+              e.preventDefault();
+              handleSave();
+            }
+          }}
         />

58-75: Broaden JSON error parsing for better messages.

Some APIs return {message: "..."} instead of {error: "..."}. Fall back to message if present.

       if (ct.includes("application/json")) {
         const data = await res.json();
         if (data && typeof data.error === "string") {
           return data.error as string;
         }
-        return JSON.stringify(data);
+        if (data && typeof data.message === "string") {
+          return data.message as string;
+        }
+        return JSON.stringify(data);
       }

135-141: Content-Type Header Semantics

I’ve confirmed that the apiPutConfig handler in proxy/proxymanager_config_api.go reads the raw request body and does not enforce the Content-Type header (it accepts either text/plain or YAML without hard-failing) and then validates the payload as YAML. Sending the config using Content-Type: "text/plain" will work correctly.

• File: ui/src/pages/ConfigEditor.tsx

  • Current fetch call (lines 135–141) uses "Content-Type": "text/plain" for the PUT to /api/config.
  • The backend ignores this header and parses the body as YAML regardless.

Optional improvement: if you’d like the header to more accurately reflect the payload, consider changing it to one of the standard YAML media types, e.g.,

- headers: { "Content-Type": "text/plain" },
+ headers: { "Content-Type": "application/x-yaml" },  // or "text/yaml"

This won’t affect functionality but provides clearer intent for proxies, middleware, and API consumers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c55d0cc and 4759f53.

📒 Files selected for processing (7)
  • llama-swap.go (3 hunks)
  • proxy/fileutil.go (1 hunks)
  • proxy/proxymanager.go (2 hunks)
  • proxy/proxymanager_api.go (1 hunks)
  • proxy/proxymanager_config_api.go (1 hunks)
  • ui/src/App.tsx (5 hunks)
  • ui/src/pages/ConfigEditor.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
proxy/proxymanager.go (1)
proxy/proxymanager_test.go (1)
  • TestProxyManager_StartupHooks (837-894)
ui/src/pages/ConfigEditor.tsx (2)
ui/src/contexts/APIProvider.tsx (2)
  • useAPI (221-227)
  • APIProvider (53-219)
ui/src/hooks/usePersistentState.ts (1)
  • usePersistentState (3-39)
proxy/proxymanager_config_api.go (5)
proxy/proxymanager.go (1)
  • ProxyManager (27-50)
proxy/config.go (1)
  • LoadConfigFromReader (198-361)
proxy/fileutil.go (1)
  • WriteFileAtomic (17-92)
event/default.go (1)
  • Emit (28-30)
proxy/events.go (3)
  • ConfigFileChangedEvent (37-39)
  • ReloadingState (30-30)
  • ReloadingStateStart (33-33)
ui/src/App.tsx (1)
ui/src/contexts/ThemeProvider.tsx (2)
  • connectionIcon (31-34)
  • ThemeProvider (24-89)
llama-swap.go (1)
proxy/proxymanager.go (1)
  • New (52-135)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-tests
🔇 Additional comments (5)
proxy/proxymanager.go (1)

45-50: Admin wiring fields look good; consider explicit "disabled" behavior when unset.

Storing configPath, reloadCallback, and watchConfigEnabled on the manager cleanly plumbs what the config API needs. Please ensure that when configPath == "" you treat the admin/config API as disabled (registration and handlers), to avoid 500s and to make intent explicit.

Would you like me to wire conditional route registration so GET/PUT /api/config are only mounted when configPath is set?

proxy/fileutil.go (1)

78-83: Windows replace semantics: confirm behavior if destination exists.

os.Rename replaces the destination atomically on Unix, but on Windows it fails if the destination exists. If Windows support matters, you may need a platform conditional to Remove first or use a safer replace pattern.

Please confirm target OS support. If Windows is in scope, I can propose a small GOOS == "windows" branch using os.Remove of the destination before rename (with appropriate failure handling).

ui/src/App.tsx (1)

53-74: Routing/nav wiring for Config page looks solid.
The new nav entry and route mount the editor cleanly under /config and respect the existing /ui basename.

Also applies to: 89-89

llama-swap.go (2)

96-99: Initial wiring looks correct.

Creating a fresh ProxyManager and wiring SetAdminControls on first load is consistent with the reload path. No issues here.


68-82: Reload semantics and raw-body handling verified, no changes needed

  • In proxy/proxymanager_config_api.go (lines 55–61), when watchConfigEnabled is false the code emits a ConfigFileChangedEvent and then calls pm.reloadCallback() as expected.
  • The request body is read unconditionally via io.ReadAll(c.Request.Body) (lines 26–28), accepting any content-type without hard failure, matching the intended “raw body” behavior.
  • SetAdminControls correctly assigns pm.watchConfigEnabled and pm.reloadCallback (in proxy/proxymanager.go, lines 633–637), ensuring the injected callback is used by apiPutConfig.

mcowger and others added 2 commits September 6, 2025 12:49
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
ui/src/pages/ConfigEditor.tsx (1)

134-151: Resolved: lost-change bug on save.

Capturing payload and setting originalContent to that exact payload fixes the in-flight edit desync flagged earlier.

🧹 Nitpick comments (6)
ui/src/pages/ConfigEditor.tsx (6)

185-192: A11y: announce status updates to screen readers.

Mark the status area as a live region so “Loading…/Saved/Error/Unsaved changes” are announced.

-        <div className="mt-2 min-h-[24px]">
+        <div className="mt-2 min-h-[24px]" role="status" aria-live="polite" aria-atomic="true">

86-90: Avoid cached GETs; be explicit about Accept.

Browsers/proxies can cache text; be explicit and disable caching.

-      const res = await fetch("/api/config", {
+      const res = await fetch("/api/config", {
         method: "GET",
         signal: controller.signal,
+        headers: { Accept: "text/plain" },
+        cache: "no-store",
       });

149-154: Clean up the “Saved” timeout to avoid setState-after-unmount.

Store/clear the timer; also clear any prior timer before setting a new one.

+  const successTimerRef = useRef<number | null>(null);
@@
-      setSuccessMsg("Saved");
-      // transient success
-      setTimeout(() => setSuccessMsg(null), 1500);
+      setSuccessMsg("Saved");
+      // transient success
+      if (successTimerRef.current) clearTimeout(successTimerRef.current);
+      successTimerRef.current = window.setTimeout(() => setSuccessMsg(null), 1500);

Add a tiny cleanup effect:

+  useEffect(() => {
+    return () => {
+      if (successTimerRef.current) clearTimeout(successTimerRef.current);
+    };
+  }, []);

128-159: Keyboard shortcut: Cmd/Ctrl+S to save.

Common affordance for editors.

+  useEffect(() => {
+    const onKeyDown = (e: KeyboardEvent) => {
+      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s") {
+        e.preventDefault();
+        if (isDirty && !saving && !loading) {
+          void handleSave();
+        }
+      }
+    };
+    window.addEventListener("keydown", onKeyDown);
+    return () => window.removeEventListener("keydown", onKeyDown);
+  }, [isDirty, saving, loading, handleSave]);

161-183: Guard against accidental navigation with unsaved changes.

Warn on tab/window close when dirty.

+  useEffect(() => {
+    const beforeUnload = (e: BeforeUnloadEvent) => {
+      if (isDirty) {
+        e.preventDefault();
+        e.returnValue = "";
+      }
+    };
+    window.addEventListener("beforeunload", beforeUnload);
+    return () => window.removeEventListener("beforeunload", beforeUnload);
+  }, [isDirty]);

167-183: Add “Revert” to discard local edits.

Handy for quickly restoring the last loaded/saved version.

           <div className="flex items-center gap-2">
             <button className="btn" onClick={toggleFontSize} title="Font size">
               <RiFontSize />
             </button>
             <button className="btn" onClick={toggleWrap} title={wrapText ? "Disable wrap" : "Enable wrap"}>
               {wrapText ? <RiTextWrap /> : <RiAlignJustify />}
             </button>
+            <button
+              className="btn"
+              onClick={() => setContent(originalContent)}
+              disabled={!isDirty || saving || loading}
+              title="Revert to last saved"
+            >
+              Revert
+            </button>
             <button
               className="btn"
               onClick={handleSave}
               disabled={!isDirty || saving || loading}
               title="Save configuration"
             >
               {saving ? "Saving..." : "Save"}
             </button>
           </div>
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between efe79b2 and 9a9d4db.

📒 Files selected for processing (1)
  • ui/src/pages/ConfigEditor.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ui/src/pages/ConfigEditor.tsx (2)
ui/src/contexts/APIProvider.tsx (2)
  • useAPI (221-227)
  • APIProvider (53-219)
ui/src/hooks/usePersistentState.ts (1)
  • usePersistentState (3-39)
🔇 Additional comments (1)
ui/src/pages/ConfigEditor.tsx (1)

77-110: Well-contained fetch lifecycle (abort + reconnect handling).

Abort-before-new-fetch, unmount cleanup, and reconnect-triggered reloads look solid. Loading disables editing, preventing read/write races during GET. Nicely done.

Also applies to: 111-118, 119-127

@mcowger
Copy link
Copy Markdown
Author

mcowger commented Sep 8, 2025

@mostlygeek - interested in merging this? I've been runing for a few weeks and its very handy.

@mostlygeek
Copy link
Copy Markdown
Owner

Hi, my apologies for not getting to this sooner. This looks like a cool feature and I’m not prepared to maintain this in the main repo at this time.

You can feel free to leave this PR open.

@mcowger
Copy link
Copy Markdown
Author

mcowger commented Sep 9, 2025

Understood. I'll keep my own fork.

@mcowger mcowger closed this Sep 9, 2025
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.

3 participants