mirror of
https://github.com/penpot/penpot.git
synced 2026-06-15 20:02:17 +00:00
✨ Add MCP status button to workspace toolbar with single-tab connection control (#9930)
* ✨ Add MCP connection badge to the workspace toolbar * ✨ Add MCP status button with single-tab connection control * ♻️ Extract component for MCP indicator in the toolbar * ♻️ Some improvements --------- Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
This commit is contained in:
parent
6a1c3348f3
commit
ddeaf3ce2a
@ -504,8 +504,7 @@
|
||||
(rx/of (dwu/append-undo entry stack-undo?)))
|
||||
(rx/empty))))))
|
||||
|
||||
(rx/take-until stoper-s))
|
||||
(rx/of (mcp/notify-other-tabs-disconnect)))))
|
||||
(rx/take-until stoper-s)))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
|
||||
@ -10,13 +10,10 @@
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.main.broadcast :as mbc]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
@ -39,6 +36,14 @@
|
||||
|
||||
(defonce interval-sub (atom nil))
|
||||
|
||||
(defn connect-mcp
|
||||
[]
|
||||
(ptk/reify ::connect-mcp
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (mbc/event :mcp/force-disconnect {})
|
||||
(ptk/data-event ::connect)))))
|
||||
|
||||
(defn finalize-workspace?
|
||||
[event]
|
||||
(= (ptk/type event) :app.main.data.workspace/finalize-workspace))
|
||||
@ -72,45 +77,6 @@
|
||||
(rx/dispose! @interval-sub)
|
||||
(reset! interval-sub nil)))
|
||||
|
||||
(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 (:connected-tab mcp-state)))
|
||||
(update state :mcp dissoc :connected-tab)
|
||||
|
||||
:else
|
||||
state)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (manage-mcp-notification)))))
|
||||
|
||||
;; This event will arrive when a new workspace is open in another tab
|
||||
(defn handle-ping
|
||||
[]
|
||||
(ptk/reify ::handle-ping
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [conn-status (get-in state [:mcp :connection-status])]
|
||||
(rx/of (mbc/event :mcp/pong {:connection-status conn-status}))))))
|
||||
|
||||
(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]
|
||||
@ -121,12 +87,10 @@
|
||||
|
||||
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)))))
|
||||
(case value
|
||||
true (rx/of (connect-mcp))
|
||||
false (rx/of (ptk/data-event ::disconnect))
|
||||
nil))))
|
||||
|
||||
(defn update-mcp-connection-status
|
||||
[value]
|
||||
@ -137,20 +101,13 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (manage-mcp-notification)
|
||||
(mbc/event :mcp/pong {:connection-status value})))))
|
||||
|
||||
(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)))))
|
||||
;; Only one MCP plugin instance may be active across browser tabs.
|
||||
;; When this tab becomes connected, tell every other tab to
|
||||
;; disconnect (which also stops their reconnect watcher). Otherwise
|
||||
;; several tabs stay connected at once and the MCP server reports
|
||||
;; "multiple instances connected" and the agent fails.
|
||||
(when (= "connected" value)
|
||||
(rx/of (mbc/event :mcp/force-disconnect {}))))))
|
||||
|
||||
;; This event will arrive when the user selects disconnect on the menu
|
||||
;; or there is a broadcast message for disconnection
|
||||
@ -166,33 +123,6 @@
|
||||
(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 "dismiss-mcp-tab-switch"
|
||||
::ev/origin "workspace-notification"}))}
|
||||
:accept {:label (tr "labels.switch")
|
||||
:callback #(st/emit! (connect-mcp)
|
||||
(ev/event {::ev/name "confirm-mcp-tab-switch"
|
||||
::ev/origin "workspace-notification"}))}})))
|
||||
(rx/of (ntf/hide)))))))
|
||||
|
||||
(defn init-mcp
|
||||
[stream]
|
||||
;; Wait for plugins runtime to be initialized before starting the MCP plugin.
|
||||
@ -240,7 +170,7 @@
|
||||
(ptk/reify ::init
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :mcp assoc :connected-tab (:session-id state) :active true))
|
||||
(update state :mcp assoc :active true))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
@ -255,22 +185,8 @@
|
||||
(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 (mbc/type? :mcp/force-disconnect))
|
||||
(rx/filter (fn [{:keys [id]}]
|
||||
(not= session-id id)))
|
||||
(rx/map deref)
|
||||
@ -280,9 +196,9 @@
|
||||
(->> 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
|
||||
;; Re-init so the force-disconnect
|
||||
;; listener is set up now that MCP
|
||||
;; is enabled.
|
||||
(rx/of (update-mcp-status true)
|
||||
(init)))))
|
||||
|
||||
@ -290,7 +206,6 @@
|
||||
(rx/filter (mbc/type? :mcp/disable))
|
||||
(rx/mapcat (fn [_]
|
||||
(rx/of (update-mcp-status false)
|
||||
(init)
|
||||
(user-disconnect-mcp))))))
|
||||
|
||||
(rx/take-until stoper-s))))))
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cph]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
@ -155,6 +156,14 @@
|
||||
(def mcp
|
||||
(l/derived :mcp st/state))
|
||||
|
||||
(def mcp-key-expired?
|
||||
(l/derived (fn [state]
|
||||
(when-let [expires-at (some->> (:access-tokens state)
|
||||
(some #(when (= (:type %) "mcp") %))
|
||||
:expires-at)]
|
||||
(> (ct/now) expires-at)))
|
||||
st/state))
|
||||
|
||||
(def workspace-drawing
|
||||
(l/derived :workspace-drawing st/state))
|
||||
|
||||
|
||||
@ -406,18 +406,16 @@
|
||||
(mf/defc mcp-server-section*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [tokens (mf/deref refs/access-tokens)
|
||||
profile (mf/deref refs/profile)
|
||||
(let [tokens (mf/deref refs/access-tokens)
|
||||
profile (mf/deref refs/profile)
|
||||
mcp-key-expired? (mf/deref refs/mcp-key-expired?)
|
||||
|
||||
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
|
||||
mcp-token (:token mcp-key "")
|
||||
mcp-url (dm/str cf/mcp-server-url "?userToken=" mcp-token)
|
||||
mcp-enabled? (true? (-> profile :props :mcp-enabled))
|
||||
|
||||
expires-at (:expires-at mcp-key)
|
||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||
|
||||
show-enabled? (and mcp-enabled? (false? expired?))
|
||||
show-enabled? (and mcp-enabled? (false? mcp-key-expired?))
|
||||
|
||||
tooltip-id
|
||||
(mf/use-id)
|
||||
@ -494,7 +492,7 @@
|
||||
(tr "integrations.mcp-server.status")]
|
||||
|
||||
[:div {:class (stl/css :mcp-server-block)}
|
||||
(when expired?
|
||||
(when mcp-key-expired?
|
||||
[:> notification-pill* {:level :error
|
||||
:type :context}
|
||||
[:div {:class (stl/css :mcp-server-notification)}
|
||||
@ -517,7 +515,7 @@
|
||||
(when (and (false? mcp-enabled?) (nil? mcp-key))
|
||||
[:div {:class (stl/css :mcp-server-switch-cover)
|
||||
:on-click handle-generate-mcp-key}])
|
||||
(when (true? expired?)
|
||||
(when (true? mcp-key-expired?)
|
||||
[:div {:class (stl/css :mcp-server-switch-cover)
|
||||
:on-click handle-regenerate-mcp-key}])]]]
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
@ -791,20 +790,15 @@
|
||||
[{:keys [on-close]}]
|
||||
(let [plugins? (features/active-feature? @st/state "plugins/runtime")
|
||||
|
||||
profile (mf/deref refs/profile)
|
||||
mcp (mf/deref refs/mcp)
|
||||
tokens (mf/deref refs/access-tokens)
|
||||
|
||||
expires-at (some->> tokens
|
||||
(some #(when (= (:type %) "mcp") %))
|
||||
:expires-at)
|
||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||
profile (mf/deref refs/profile)
|
||||
mcp (mf/deref refs/mcp)
|
||||
mcp-key-expired? (mf/deref refs/mcp-key-expired?)
|
||||
|
||||
mcp-enabled? (true? (-> profile :props :mcp-enabled))
|
||||
mcp-connection (get mcp :connection-status)
|
||||
mcp-connected? (= mcp-connection "connected")
|
||||
|
||||
show-enabled? (and mcp-enabled? (false? expired?))
|
||||
show-enabled? (and mcp-enabled? (false? mcp-key-expired?))
|
||||
|
||||
on-nav-to-integrations
|
||||
(mf/use-fn
|
||||
@ -843,7 +837,7 @@
|
||||
:pos-6 plugins?)
|
||||
:on-close on-close}
|
||||
|
||||
(when (and show-enabled? (not expired?))
|
||||
(when (and show-enabled? (not mcp-key-expired?))
|
||||
[:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin"
|
||||
:class (stl/css :base-menu-item :submenu-item)
|
||||
:on-click on-toggle-mcp-plugin
|
||||
@ -865,7 +859,6 @@
|
||||
(mf/defc menu*
|
||||
[{:keys [layout file]}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
mcp (mf/deref refs/mcp)
|
||||
|
||||
show-menu* (mf/use-state false)
|
||||
show-menu? (deref show-menu*)
|
||||
@ -1062,11 +1055,8 @@
|
||||
:class (stl/css :item-arrow)}]])
|
||||
|
||||
(when (contains? cf/flags :mcp)
|
||||
(let [tokens (mf/deref refs/access-tokens)
|
||||
expires-at (some->> tokens
|
||||
(some #(when (= (:type %) "mcp") %))
|
||||
:expires-at)
|
||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||
(let [mcp (mf/deref refs/mcp)
|
||||
mcp-key-expired? (mf/deref refs/mcp-key-expired?)
|
||||
|
||||
mcp-enabled? (true? (-> profile :props :mcp-enabled))
|
||||
mcp-connection (get mcp :connection-status)
|
||||
@ -1075,7 +1065,7 @@
|
||||
|
||||
active? (and mcp-enabled? mcp-connected?)
|
||||
failed? (or (and mcp-enabled? mcp-error?)
|
||||
(true? expired?))]
|
||||
(true? mcp-key-expired?))]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item)
|
||||
:on-click on-menu-click
|
||||
|
||||
@ -8,15 +8,18 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.mcp :as mcp]
|
||||
[app.main.data.workspace.media :as dwm]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
@ -26,6 +29,61 @@
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc mcp-indicator*
|
||||
[]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
mcp (mf/deref refs/mcp)
|
||||
mcp-key-expired? (mf/deref refs/mcp-key-expired?)
|
||||
|
||||
mcp-enabled? (true? (-> profile :props :mcp-enabled))
|
||||
mcp-connected? (= "connected" (:connection-status mcp))
|
||||
show-indicator? (and mcp-enabled? (false? mcp-key-expired?))
|
||||
|
||||
mcp-menu-open* (mf/use-state false)
|
||||
mcp-menu-open? (deref mcp-menu-open*)
|
||||
|
||||
toggle-mcp-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! mcp-menu-open* not)))
|
||||
|
||||
close-mcp-menu
|
||||
(mf/use-fn
|
||||
#(reset! mcp-menu-open* false))
|
||||
|
||||
connect-mcp
|
||||
(mf/use-fn
|
||||
#(st/emit! (mcp/connect-mcp)
|
||||
(ev/event {::ev/name "connect-mcp-plugin"
|
||||
::ev/origin "workspace:toolbar"})))]
|
||||
(when show-indicator?
|
||||
[:li
|
||||
[:button
|
||||
{:title (tr "workspace.toolbar.mcp")
|
||||
:aria-label (tr "workspace.toolbar.mcp")
|
||||
:class (stl/css-case :main-toolbar-options-button true
|
||||
:mcp-button true
|
||||
:selected mcp-menu-open?)
|
||||
:on-click toggle-mcp-menu
|
||||
:data-tool "mcp"
|
||||
:data-testid "mcp-btn"}
|
||||
[:span {:class (stl/css-case :mcp-status-dot true
|
||||
:connected mcp-connected?)}]
|
||||
[:span {:class (stl/css-case :mcp-button-label true
|
||||
:connected mcp-connected?)}
|
||||
(tr "workspace.toolbar.mcp")]]
|
||||
[:> dropdown-menu* {:show mcp-menu-open?
|
||||
:on-close close-mcp-menu
|
||||
:class (stl/css :mcp-menu)}
|
||||
(if mcp-connected?
|
||||
[:li {:class (stl/css :mcp-menu-info)
|
||||
:role "presentation"}
|
||||
(tr "workspace.toolbar.mcp-connected")]
|
||||
[:> dropdown-menu-item* {:class (stl/css :mcp-menu-item)
|
||||
:on-click connect-mcp}
|
||||
(tr "workspace.toolbar.mcp-connect-here")])]])))
|
||||
|
||||
(mf/defc image-upload*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[]
|
||||
@ -221,12 +279,13 @@
|
||||
{:title "Debugging tool"
|
||||
:class (stl/css-case :main-toolbar-options-button true :selected (contains? layout :debug-panel))
|
||||
:on-click toggle-debug-panel}
|
||||
deprecated-icon/bug]])]]
|
||||
deprecated-icon/bug]])
|
||||
|
||||
(when (contains? cf/flags :mcp)
|
||||
[:> mcp-indicator*])]]
|
||||
|
||||
[:button {:title (tr "workspace.toolbar.toggle-toolbar")
|
||||
:aria-label (tr "workspace.toolbar.toggle-toolbar")
|
||||
:class (stl/css :toolbar-handler)
|
||||
:on-click toggle-toolbar}
|
||||
[:div {:class (stl/css :toolbar-handler-btn)}]]])))
|
||||
|
||||
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.main-toolbar {
|
||||
cursor: initial;
|
||||
@ -83,6 +86,102 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
width: fit-content;
|
||||
margin-inline-start: var(--sp-xs);
|
||||
padding-inline: var(--sp-s);
|
||||
}
|
||||
|
||||
.mcp-button-label {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
&.connected {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-status-dot {
|
||||
// Connection indicator placed before the label, vertically centered:
|
||||
// a muted gray when disconnected, the primary accent when connected.
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
inline-size: px2rem(6);
|
||||
block-size: px2rem(6);
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-background-disabled);
|
||||
|
||||
&.connected {
|
||||
background-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
// One-shot "blob" ripple that confirms the moment the tab becomes
|
||||
// connected (e.g. after using "Switch here"). Triggered purely by the
|
||||
// `.connected` class appearing, in sync with the color change.
|
||||
&.connected::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-accent-primary);
|
||||
opacity: 0;
|
||||
animation: mcp-status-blob 0.5s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mcp-status-blob {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-menu {
|
||||
@include deprecated.menu-shadow;
|
||||
|
||||
position: absolute;
|
||||
inset-block-start: calc(100% + var(--sp-xs));
|
||||
inset-inline-start: var(--sp-xs);
|
||||
z-index: var(--z-index-dropdown);
|
||||
margin: 0;
|
||||
padding: var(--sp-xs);
|
||||
list-style: none;
|
||||
border: $b-1 solid var(--panel-border-color);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--menu-background-color);
|
||||
}
|
||||
|
||||
// Non-interactive informational text inside the menu (no hover, not focusable).
|
||||
.mcp-menu-info {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
padding: var(--sp-s) var(--sp-m);
|
||||
color: var(--color-foreground-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mcp-menu-item {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--sp-s) var(--sp-m);
|
||||
border-radius: $br-8;
|
||||
color: var(--menu-foreground-color);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-handler {
|
||||
@include deprecated.flex-center;
|
||||
@include deprecated.button-style;
|
||||
|
||||
50
frontend/test/frontend_tests/data/workspace_mcp_test.cljs
Normal file
50
frontend/test/frontend_tests/data/workspace_mcp_test.cljs
Normal file
@ -0,0 +1,50 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.data.workspace-mcp-test
|
||||
(:require
|
||||
[app.main.data.workspace.mcp :as mcp]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/deftest test-set-mcp-active
|
||||
(t/testing "sets :active to true"
|
||||
(let [state {:mcp {:active false}}
|
||||
result (ptk/update (mcp/set-mcp-active true) state)]
|
||||
(t/is (true? (get-in result [:mcp :active])))))
|
||||
|
||||
(t/testing "sets :active to false"
|
||||
(let [state {:mcp {:active true}}
|
||||
result (ptk/update (mcp/set-mcp-active false) state)]
|
||||
(t/is (false? (get-in result [:mcp :active]))))))
|
||||
|
||||
(t/deftest test-update-mcp-status
|
||||
(t/testing "enables MCP in profile props"
|
||||
(let [state {:profile {:props {:mcp-enabled false}}}
|
||||
result (ptk/update (mcp/update-mcp-status true) state)]
|
||||
(t/is (true? (get-in result [:profile :props :mcp-enabled])))))
|
||||
|
||||
(t/testing "disables MCP in profile props"
|
||||
(let [state {:profile {:props {:mcp-enabled true}}}
|
||||
result (ptk/update (mcp/update-mcp-status false) state)]
|
||||
(t/is (false? (get-in result [:profile :props :mcp-enabled]))))))
|
||||
|
||||
(t/deftest test-update-mcp-connection-status
|
||||
(t/testing "sets connection status to connected"
|
||||
(let [state {:mcp {:connection-status "disconnected"}}
|
||||
result (ptk/update (mcp/update-mcp-connection-status "connected") state)]
|
||||
(t/is (= "connected" (get-in result [:mcp :connection-status])))))
|
||||
|
||||
(t/testing "sets connection status to disconnected"
|
||||
(let [state {:mcp {:connection-status "connected"}}
|
||||
result (ptk/update (mcp/update-mcp-connection-status "disconnected") state)]
|
||||
(t/is (= "disconnected" (get-in result [:mcp :connection-status]))))))
|
||||
|
||||
(t/deftest test-init-sets-active
|
||||
(t/testing "init sets :mcp :active to true"
|
||||
(let [state {:mcp {:active false}}
|
||||
result (ptk/update (mcp/init) state)]
|
||||
(t/is (true? (get-in result [:mcp :active]))))))
|
||||
@ -7,6 +7,7 @@
|
||||
[frontend-tests.data.uploads-test]
|
||||
[frontend-tests.data.viewer-test]
|
||||
[frontend-tests.data.workspace-colors-test]
|
||||
[frontend-tests.data.workspace-mcp-test]
|
||||
[frontend-tests.data.workspace-media-test]
|
||||
[frontend-tests.data.workspace-texts-test]
|
||||
[frontend-tests.data.workspace-thumbnails-test]
|
||||
@ -55,6 +56,7 @@
|
||||
'frontend-tests.data.uploads-test
|
||||
'frontend-tests.data.viewer-test
|
||||
'frontend-tests.data.workspace-colors-test
|
||||
'frontend-tests.data.workspace-mcp-test
|
||||
'frontend-tests.data.workspace-media-test
|
||||
'frontend-tests.data.workspace-texts-test
|
||||
'frontend-tests.data.workspace-thumbnails-test
|
||||
|
||||
@ -4043,9 +4043,6 @@ msgstr "Invitation sent successfully"
|
||||
msgid "notifications.invitation-link-copied"
|
||||
msgstr "Invitation link copied"
|
||||
|
||||
msgid "notifications.mcp.active-in-another-tab"
|
||||
msgstr "MCP is active in another tab. Switch here?"
|
||||
|
||||
msgid "notifications.mcp.active-in-this-tab"
|
||||
msgstr "MCP is now active in this tab."
|
||||
|
||||
@ -8986,6 +8983,15 @@ msgstr "Create board. Click and drag to define its size. (%s)"
|
||||
msgid "workspace.toolbar.image"
|
||||
msgstr "Image (%s)"
|
||||
|
||||
msgid "workspace.toolbar.mcp"
|
||||
msgstr "MCP"
|
||||
|
||||
msgid "workspace.toolbar.mcp-connected"
|
||||
msgstr "MCP connected"
|
||||
|
||||
msgid "workspace.toolbar.mcp-connect-here"
|
||||
msgstr "Connect here"
|
||||
|
||||
#: src/app/main/ui/workspace/top_toolbar.cljs:143
|
||||
msgid "workspace.toolbar.move"
|
||||
msgstr "Move (%s)"
|
||||
|
||||
@ -3952,9 +3952,6 @@ msgstr "Invitación enviada con éxito"
|
||||
msgid "notifications.invitation-link-copied"
|
||||
msgstr "Enlace de invitacion copiado"
|
||||
|
||||
msgid "notifications.mcp.active-in-another-tab"
|
||||
msgstr "MCP está activo en otra pestaña. ¿Cambiar a esta?"
|
||||
|
||||
msgid "notifications.mcp.active-in-this-tab"
|
||||
msgstr "MCP está ahora activo en esta pestaña."
|
||||
|
||||
@ -8708,6 +8705,15 @@ msgstr "Crear tablero. Click y arrastrar para definir el tamaño. (%s)"
|
||||
msgid "workspace.toolbar.image"
|
||||
msgstr "Imagen (%s)"
|
||||
|
||||
msgid "workspace.toolbar.mcp"
|
||||
msgstr "MCP"
|
||||
|
||||
msgid "workspace.toolbar.mcp-connected"
|
||||
msgstr "MCP conectado"
|
||||
|
||||
msgid "workspace.toolbar.mcp-connect-here"
|
||||
msgstr "Conectar aquí"
|
||||
|
||||
#: src/app/main/ui/workspace/top_toolbar.cljs:143
|
||||
msgid "workspace.toolbar.move"
|
||||
msgstr "Mover (%s)"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user