Skip to content

Commit 8f95842

Browse files
committed
Fix empty Settings -> MCPs tab on tool-window reopen (#22)
The host caches tool/serverUpdated notifications under [:session :mcp-servers] but only re-broadcast them reactively, so the React mcp.servers slice sat empty whenever the tool window was re-opened (or the user navigated to Settings -> MCPs) after all notifications had already fired. webview/ready now replays the cached roster as tool/serversUpdated. Also stops on-initialized from clobbering the whole :session map with (fn [_] {...}). It now merges, so notifications that arrive ahead of the initialized reply (cached MCP servers come up fast) no longer get wiped. Closes #22.
1 parent 75e198a commit 8f95842

5 files changed

Lines changed: 95 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Fix empty Settings → MCPs tab on tool-window reopen (#22). `webview/ready` now replays the cached MCP roster as `tool/serversUpdated`, and `on-initialized` merges into `:session` instead of clobbering it (notifications that race ahead of `initialized` no longer get wiped).
56
- Bump `eca-webview`: shrink the welcome-message logo and left-align the welcome text block, while keeping the logo and any markdown heading centered.
67

78
## 0.26.13

src/main/clojure/dev/eca/eca_intellij/server.clj

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,20 @@
105105
(logger/info "Downloaded eca to" dest-path)))
106106

107107
(defn ^:private on-initialized [result project]
108-
(db/update-in project [:session] (fn [_]
109-
{:models (:models result)
110-
:chat-agents (:chat-agents result)
111-
:chat-selected-agent (:chat-default-agent result)
112-
:chat-selected-model (:chat-default-model result)
113-
:welcome-message (:chat-welcome-message result)})))
108+
;; Merge on top of the existing session map instead of replacing it
109+
;; wholesale -- a wholesale replace silently wipes any keys populated
110+
;; before `initialized` completes (notably `:mcp-servers`, which
111+
;; `webview.clj`'s tool-server-updated defmethod fills in from
112+
;; reactive `tool/serverUpdated` notifications that can race ahead of
113+
;; this callback). Wiping that map then leaves the Settings -> MCPs
114+
;; tab empty after webview/ready replay until the next notification
115+
;; arrives. Closes #22.
116+
(db/update-in project [:session]
117+
#(merge % {:models (:models result)
118+
:chat-agents (:chat-agents result)
119+
:chat-selected-agent (:chat-default-agent result)
120+
:chat-selected-model (:chat-default-model result)
121+
:welcome-message (:chat-welcome-message result)})))
114122

115123
(defn ^:private env []
116124
(let [env (EnvironmentUtil/getEnvironmentMap)

src/main/clojure/dev/eca/eca_intellij/webview.clj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@
189189
(handle-server-status-changed (db/get-in project [:status])
190190
project)
191191
(handle-config-changed project (db/get-in project [:server-config]))
192+
;; Replay the cached MCP server roster. `tool/serverUpdated`
193+
;; notifications from the ECA server are reactive-only -- if the
194+
;; tool window is re-opened (or the user navigates to Settings ->
195+
;; MCPs) after all notifications have already fired, the React
196+
;; Redux slice would otherwise sit empty until the next change.
197+
;; Mirrors the broadcast shape used by tool-server-updated /
198+
;; tool-server-removed so consumers converge through the same code
199+
;; path. `vec` so an empty cache serialises as `[]` instead of
200+
;; `null`, which the React slice cannot iterate over. Closes #22.
201+
(send-msg! project {:type "tool/serversUpdated"
202+
:data (vec (vals (db/get-in project [:session :mcp-servers])))})
192203
;; send current opened editor if any
193204
(when-let [editor (current-selected-editor project)]
194205
(on-focus-changed editor nil))

src/test/clojure/dev/eca/eca_intellij/server_lifecycle_test.clj

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,32 @@
6060
(fixt/with-test-project [project
6161
:initial-db {:status :starting}]
6262
(is (= :starting (server/status project)))))
63+
64+
(deftest on-initialized-preserves-mcp-servers-cache
65+
(testing "Regression: issue #22. Before the fix, on-initialized did
66+
`(update-in [:session] (fn [_] {...}))` which wiped any
67+
keys populated before `initialized` completed. The ECA
68+
server can send `tool/serverUpdated` notifications ahead
69+
of the `initialized` reply (especially for cached MCP
70+
servers that come up fast), so wiping `:mcp-servers`
71+
here was the root cause of the empty Settings -> MCPs
72+
tab. on-initialized must MERGE into the existing
73+
session, not replace it."
74+
(fixt/with-test-project [project
75+
:initial-db {:session
76+
{:mcp-servers
77+
{"foo" {:name "foo"
78+
:status "running"}}}}]
79+
(#'server/on-initialized {:models [{:id "claude"}]
80+
:chat-agents [{:id "agent"}]
81+
:chat-default-agent "agent"
82+
:chat-default-model "claude"
83+
:chat-welcome-message "hi"}
84+
project)
85+
(let [session (db/get-in project [:session])]
86+
(is (= {"foo" {:name "foo" :status "running"}}
87+
(:mcp-servers session))
88+
":mcp-servers populated before initialized MUST survive")
89+
(is (= [{:id "claude"}] (:models session)))
90+
(is (= "claude" (:chat-selected-model session)))
91+
(is (= "hi" (:welcome-message session)))))))

src/test/clojure/dev/eca/eca_intellij/webview_lifecycle_test.clj

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,46 @@
7373
":webview key in :on-focus-changed-fns is what cursor-editor-listener fans
7474
out to; missing -> no live editor-focus updates to the chat"))))
7575

76+
(deftest webview-ready-replays-cached-mcp-servers
77+
(testing "Regression: issue #22. `tool/serverUpdated` LSP
78+
notifications are reactive-only; if the tool window is
79+
re-opened (or the user opens Settings -> MCPs) after all
80+
notifications have already fired, the React `mcp.servers`
81+
slice would otherwise sit empty. `webview/ready` must
82+
replay the cached roster as `tool/serversUpdated` so the
83+
settings tab is never blank when servers actually exist."
84+
(fixt/with-test-project [project
85+
:initial-db {:session
86+
{:mcp-servers
87+
{"foo" {:name "foo"
88+
:type "mcp"
89+
:status "running"
90+
:tools []}
91+
"bar" {:name "bar"
92+
:type "mcp"
93+
:status "stopped"
94+
:tools []}}}}]
95+
(fixt/with-stub-bridge bridge
96+
(webview/handle (fixt/to-json-payload {:type "webview/ready"}) project)
97+
(let [out (fixt/last-to-webview-of-type bridge "tool/serversUpdated")]
98+
(is (some? out)
99+
"webview/ready MUST emit tool/serversUpdated even with no live notification")
100+
(is (= #{"foo" "bar"}
101+
(->> (:data out) (map :name) set))
102+
"all cached MCP servers must be replayed"))))))
103+
104+
(deftest webview-ready-emits-empty-mcp-list-when-no-servers
105+
(testing "With an empty cache the replay still fires (just with an
106+
empty list). This keeps the webview's mcp.servers slice
107+
consistent with the host's truth-of-record on every ready
108+
handshake and lets the React side clear any stale rows."
109+
(fixt/with-test-project [project]
110+
(fixt/with-stub-bridge bridge
111+
(webview/handle (fixt/to-json-payload {:type "webview/ready"}) project)
112+
(let [out (fixt/last-to-webview-of-type bridge "tool/serversUpdated")]
113+
(is (some? out))
114+
(is (= [] (:data out))))))))
115+
76116
(deftest webview-ready-subscribes-log-store-with-replace-semantics
77117
(testing "Re-delivery of webview/ready (e.g. tool-window re-open) must
78118
replace the prior :webview log subscriber, not stack a

0 commit comments

Comments
 (0)