🐛 Fix MCP active tab switching (#8856)

This commit is contained in:
Luis de Dios 2026-04-07 10:58:04 +02:00 committed by GitHub
parent 67734c5835
commit e99b6ec213
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 330 additions and 243 deletions

View File

@ -57,5 +57,6 @@
[type data]
(ptk/reify ::event
ptk/EffectEvent
(effect [_ _ _]
(emit! type data))))
(effect [_ state _]
(let [session-id (get state :session-id)]
(emit! session-id type data)))))

View File

@ -25,7 +25,6 @@
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.broadcast :as mbc]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
@ -213,9 +212,11 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts-for-local-library file-id)
(mcp/init-mcp-connection)))))
(rx/merge
(rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts-for-local-library file-id))
(when (contains? cf/flags :mcp)
(rx/of (mcp/init)))))))
(defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}]
@ -319,180 +320,169 @@
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(->> (rx/merge
(rx/concat
;; Fetch all essential data that should be loaded before the file
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/filter true?)
(rx/tap (fn [_]
(let [event (ug/event "penpot:wasm:loaded")]
(ug/dispatch! event))))
(rx/ignore))
(rx/empty))
(rx/concat
(->> (rx/merge
(rx/concat
;; Fetch all essential data that should be loaded before the file
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/filter true?)
(rx/tap (fn [_]
(let [event (ug/event "penpot:wasm:loaded")]
(ug/dispatch! event))))
(rx/ignore))
(rx/empty))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(df/fetch-fonts team-id))
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(df/fetch-fonts team-id))
(when (contains? cf/flags :mcp)
(rx/of (du/fetch-access-tokens))))
(when (contains? cf/flags :mcp)
(rx/of (du/fetch-access-tokens))))
;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features)))
;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features)))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat
(fn [{:keys [file]}]
(log/debug :hint "bundle fetched"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat
(fn [{:keys [file]}]
(log/debug :hint "bundle fetched"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id features)
(-> (workspace-initialized file-id)
(with-meta {:team-id team-id
:file-id file-id}))))))
(rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id features)
(-> (workspace-initialized file-id)
(with-meta {:team-id team-id
:file-id file-id}))))))
;; Install dev perf observers once the workspace is ready
(when (contains? cf/flags :perf-logs)
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/take 1)
(rx/tap (fn [_] (perf/setup)))))
;; Install dev perf observers once the workspace is ready
(when (contains? cf/flags :perf-logs)
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/take 1)
(rx/tap (fn [_] (perf/setup)))))
(when (contains? cf/flags :mcp)
(->> mbc/stream
(rx/filter (mbc/type? :mcp-enabled-change-connection))
(rx/map deref)
(rx/mapcat (fn [value]
(rx/of (mcp/update-mcp-connection value)
(mcp/user-disconnect-mcp))))))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(when (contains? cf/flags :mcp)
(->> mbc/stream
(rx/filter (mbc/type? :mcp-enabled-change-status))
(rx/map deref)
(rx/map mcp/update-mcp-status)))
(when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(when (:board-id rparams)
(->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1)
(rx/map zoom-to-frame)))
(when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when (:board-id rparams)
(->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1)
(rx/map zoom-to-frame)))
(when render-wasm?
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [redo-changes]}]
(let [added (->> redo-changes
(filter #(= (:type %) :add-obj))
(map :id))]
(->> (rx/from added)
(rx/map process-wasm-object)))))))
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
(when render-wasm?
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [redo-changes]}]
(let [added (->> redo-changes
(filter #(= (:type %) :add-obj))
(map :id))]
(->> (rx/from added)
(rx/map process-wasm-object)))))))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s))))
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(rx/take-until stoper-s))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s))))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
(rx/take-until stoper-s))))
(rx/of (mcp/notify-other-tabs-disconnect)))))
ptk/EffectEvent
(effect [_ _ _]

View File

@ -48,7 +48,7 @@
(ptk/reify ::set-mcp-active
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :mcp :active] value))))
(assoc-in state [:mcp :active] value))))
(defn start-reconnect-watcher!
[]
@ -61,7 +61,7 @@
(fn []
;; Try to reconnect if active and not connected
(when-not (contains? #{"connecting" "connected"}
(-> @st/state :workspace-local :mcp :connection))
(-> @st/state :mcp :connection-status))
(.log js/console "Reconnecting to MCP...")
(st/emit! (ptk/data-event ::connect))))))))
@ -72,55 +72,46 @@
(rx/dispose! @interval-sub)
(reset! interval-sub nil)))
;; This event will arrive when the user selects disconnect on the menu
;; or there is a broadcast message for disconnection
(defn user-disconnect-mcp
[]
(ptk/reify ::remote-disconnect-mcp
(declare manage-mcp-notification)
(defn handle-pong
[{:keys [id data]}]
(ptk/reify ::handle-pong
ptk/UpdateEvent
(update [_ state]
(let [mcp-state (get state :mcp)]
(cond
(= "connected" (:connection-status data))
(update state :mcp assoc :connected-tab id)
(and (= "disconnected" (:connection-status data))
(= id (:connection-status mcp-state)))
(update state :mcp dissoc :connected-tab)
:else
state)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (ptk/data-event ::disconnect)))
(rx/of (manage-mcp-notification)))))
ptk/EffectEvent
(effect [_ _ _]
(stop-reconnect-watcher!))))
(defn connect-mcp
;; This event will arrive when a new workspace is open in another tab
(defn handle-ping
[]
(ptk/reify ::connect-mcp
ptk/WatchEvent
(watch [_ _ stream]
(mbc/emit! :mcp-enabled-change-connection false)
(->> stream
(rx/filter (ptk/type? ::disconnect))
(rx/take 1)
(rx/map #(ptk/data-event ::connect))))))
(defn manage-mcp-notification
[]
(ptk/reify ::manage-mcp-notification
(ptk/reify ::handle-ping
ptk/WatchEvent
(watch [_ state _]
(let [mcp-connected? (= "connected" (-> state :workspace-local :mcp :connection))
mcp-enabled? (true? (-> state :profile :props :mcp-enabled))
num-sessions (-> state :workspace-presence count)
multi-session? (> num-sessions 1)
mcp-active? (-> state :workspace-local :mcp :active)]
(if (and mcp-enabled? multi-session?)
(if (or mcp-connected? mcp-active?)
(rx/of (ntf/hide))
(rx/of (ntf/dialog :content (tr "notifications.mcp.active-in-another-tab")
:cancel {:label (tr "labels.dismiss")
:callback #(st/emit! (ntf/hide)
(ptk/event ::ev/event {::ev/name "confirm-mcp-tab-switch"
::ev/origin "workspace-notification"}))}
:accept {:label (tr "labels.switch")
:callback #(st/emit! (connect-mcp)
(ptk/event ::ev/event {::ev/name "dismiss-mcp-tab-switch"
::ev/origin "workspace-notification"}))})))
(rx/of (ntf/hide)))))))
(let [conn-status (get-in state [:mcp :connection-status])]
(rx/of (mbc/event :mcp/pong {:connection-status conn-status}))))))
;; This event will arrive when the mcp is enabled in the main menu
(defn notify-other-tabs-disconnect
[]
(ptk/reify ::notify-other-tabs-disconnect
ptk/WatchEvent
(watch [_ _ _]
(rx/of (mbc/event :mcp/pong {:connection-status "disconnected"})))))
;; This event will arrive when the mcp is enabled in the dashboard
(defn update-mcp-status
[value]
(ptk/reify ::update-mcp-status
@ -131,27 +122,81 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/merge
(rx/of (manage-mcp-notification)))
(case value
true (rx/of (ptk/data-event ::connect))
false (rx/of (ptk/data-event ::disconnect))
nil))))
(rx/of (manage-mcp-notification))
(case value
true (rx/of (ptk/data-event ::connect))
false (rx/of (ptk/data-event ::disconnect))
nil)))))
(defn update-mcp-connection
(defn update-mcp-connection-status
[value]
(ptk/reify ::update-mcp-plugin-connection
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :mcp] assoc :connection value))
(update state :mcp assoc :connection-status value))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (manage-mcp-notification)))))
(rx/of (manage-mcp-notification)
(mbc/event :mcp/pong {:connection-status value})))))
(defn init-mcp!
(defn connect-mcp
[]
(ptk/reify ::connect-mcp
ptk/UpdateEvent
(update [_ state]
(update state :mcp assoc :connected-tab (:session-id state)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (mbc/event :mcp/force-disconect {})
(ptk/data-event ::connect)))))
;; This event will arrive when the user selects disconnect on the menu
;; or there is a broadcast message for disconnection
(defn user-disconnect-mcp
[]
(ptk/reify ::user-disconnect-mcp
ptk/WatchEvent
(watch [_ _ _]
(rx/of (ptk/data-event ::disconnect)
(update-mcp-connection-status "disconnected")))
ptk/EffectEvent
(effect [_ _ _]
(stop-reconnect-watcher!))))
(defn- manage-mcp-notification
[]
(ptk/reify ::manage-mcp-notification
ptk/WatchEvent
(watch [_ state _]
(let [mcp-state (get state :mcp)
mcp-enabled? (-> state :profile :props :mcp-enabled)
current-tab-id (get state :session-id)
connected-tab-id (get mcp-state :connected-tab)]
(if mcp-enabled?
(if (= connected-tab-id current-tab-id)
(rx/of (ntf/hide))
(rx/of (ntf/dialog
{:content (tr "notifications.mcp.active-in-another-tab")
:cancel {:label (tr "labels.dismiss")
:callback #(st/emit! (ntf/hide)
(ev/event {::ev/name "confirm-mcp-tab-switch"
::ev/origin "workspace-notification"}))}
:accept {:label (tr "labels.switch")
:callback #(st/emit! (connect-mcp)
(ev/event {::ev/name "dismiss-mcp-tab-switch"
::ev/origin "workspace-notification"}))}})))
(rx/of (ntf/hide)))))))
(defn init-mcp
[stream]
(->> (rp/cmd! :get-current-mcp-token)
(rx/subs!
(rx/tap
(fn [{:keys [token]}]
(when token
(dp/start-plugin!
@ -168,7 +213,7 @@
(fn [status]
(when (= status "connected")
(start-reconnect-watcher!))
(st/emit! (update-mcp-connection status))
(st/emit! (update-mcp-connection-status status))
(log/info :hint "MCP STATUS" :status status))
:on
@ -183,13 +228,65 @@
(->> stream
(rx/filter (ptk/type? event))
(rx/take-until stopper)
(rx/subs! #(cb))))))}}))))))
(rx/subs! #(cb))))))}}))))
(rx/ignore)))
(defn init-mcp-connection
(defn init
[]
(ptk/reify ::init-mcp-connection
ptk/EffectEvent
(effect [_ state stream]
(when (and (contains? cf/flags :mcp)
(-> state :profile :props :mcp-enabled))
(init-mcp! stream)))))
(ptk/reify ::init
ptk/UpdateEvent
(update [_ state]
(update state :mcp assoc :connected-tab (:session-id state) :active true))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper-s (rx/merge
(rx/filter (ptk/type? :app.main.data.workspace/finalize-workspace) stream)
(rx/filter (ptk/type? ::init) stream))
session-id (get state :session-id)
enabled? (-> state :profile :props :mcp-enabled)]
(->> (rx/merge
(if enabled?
(rx/merge
(init-mcp stream)
(rx/of (mbc/event :mcp/ping {}))
(->> mbc/stream
(rx/filter (mbc/type? :mcp/ping))
(rx/filter (fn [{:keys [id]}]
(not= session-id id)))
(rx/map handle-ping))
(->> mbc/stream
(rx/filter (mbc/type? :mcp/pong))
(rx/filter (fn [{:keys [id]}]
(not= session-id id)))
(rx/map handle-pong))
(->> mbc/stream
(rx/filter (mbc/type? :mcp/force-disconect))
(rx/filter (fn [{:keys [id]}]
(not= session-id id)))
(rx/map deref)
(rx/map (fn [] (user-disconnect-mcp)))))
(rx/empty))
(->> mbc/stream
(rx/filter (mbc/type? :mcp/enable))
(rx/mapcat (fn [_]
;; NOTE: we don't need an explicit
;; connect because the plugin has
;; auto-connect
(rx/of (update-mcp-status true)
(init)))))
(->> mbc/stream
(rx/filter (mbc/type? :mcp/disable))
(rx/mapcat (fn [_]
(rx/of (update-mcp-status false)
(init)
(user-disconnect-mcp))))))
(rx/take-until stoper-s))))))

View File

@ -12,7 +12,6 @@
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.common :as dc]
[app.main.data.helpers :as dsh]
@ -24,7 +23,6 @@
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.layout :as dwly]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.mcp :as mcp]
[app.main.data.workspace.texts :as dwt]
[app.main.router :as rt]
[app.util.globals :refer [global]]
@ -214,12 +212,8 @@
(update [_ state]
(if (or (= :disconnect type) (= :leave-file type))
(update state :workspace-presence dissoc session-id)
(update state :workspace-presence update-presence)))
(update state :workspace-presence update-presence))))))
ptk/WatchEvent
(watch [_ _ _]
(when (contains? cf/flags :mcp)
(rx/of (mcp/manage-mcp-notification)))))))
(defn handle-pointer-update
[{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}]

View File

@ -150,6 +150,9 @@
(def workspace-global
(l/derived :workspace-global st/state))
(def mcp
(l/derived :mcp st/state))
(def workspace-drawing
(l/derived :workspace-drawing st/state))

View File

@ -285,8 +285,8 @@
::ev/origin "integrations"})
(ev/event {::ev/name "enable-mcp"
::ev/origin "integrations"
:source "key-creation"}))
(mbc/emit! :mcp-enabled-change-status true)
:source "key-creation"})
(mbc/event :mcp/enable {}))
(reset! created? true)))]
[:div {:class (stl/css :modal-overlay)}
@ -326,8 +326,8 @@
(st/emit! (du/delete-access-token {:id mcp-key-id})
(du/update-profile-props {:mcp-enabled true})
(ev/event {::ev/name "regenerate-mcp-key"
::ev/origin "integrations"}))
(mbc/emit! :mcp-enabled-change-status true)
::ev/origin "integrations"})
(mbc/event :mcp/enable {}))
(reset! created? true)))]
[:div {:class (stl/css :modal-overlay)}
@ -437,8 +437,10 @@
:timeout notification-timeout})
(ev/event {::ev/name (if (true? value) "enable-mcp" "disable-mcp")
::ev/origin "integrations"
:source "toggle"}))
(mbc/emit! :mcp-enabled-change-status value)))
:source "toggle"})
(if value
(mbc/event :mcp/enable {})
(mbc/event :mcp/disable {})))))
handle-generate-mcp-key
(mf/use-fn
@ -455,8 +457,8 @@
(let [params {:id (:id mcp-key)}
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
(st/emit! (du/delete-access-token (with-meta params mdata))
(du/update-profile-props {:mcp-enabled false}))
(mbc/emit! :mcp-enabled-change-status false))))
(du/update-profile-props {:mcp-enabled false})
(mbc/event :mcp/disable {})))))
on-copy-to-clipboard
(mf/use-fn

View File

@ -752,10 +752,10 @@
(let [plugins? (features/active-feature? @st/state "plugins/runtime")
profile (mf/deref refs/profile)
workspace-local (mf/deref refs/workspace-local)
mcp (mf/deref refs/mcp)
mcp-enabled? (true? (-> profile :props :mcp-enabled))
mcp-connected? (= "connected" (-> workspace-local :mcp :connection))
mcp-connected? (= "connected" (get mcp :connection-status))
on-nav-to-integrations
(mf/use-fn
@ -815,8 +815,8 @@
(mf/defc menu*
[{:keys [layout file]}]
(let [profile (mf/deref refs/profile)
workspace-local (mf/deref refs/workspace-local)
(let [profile (mf/deref refs/profile)
mcp (mf/deref refs/mcp)
show-menu* (mf/use-state false)
show-menu? (deref show-menu*)
@ -990,7 +990,7 @@
(> (ct/now)))
mcp-enabled? (true? (-> profile :props :mcp-enabled))
mcp-connection (-> workspace-local :mcp :connection)
mcp-connection (get mcp :connection-status)
mcp-connected? (= mcp-connection "connected")
mcp-error? (= mcp-connection "error")