Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion packages/app/src/components/status-popover.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Popover } from "@opencode-ai/ui/popover"
import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
Expand Down Expand Up @@ -191,6 +192,8 @@ export function StatusPopover() {
const plugins = createMemo(() => sync.data.config.plugin ?? [])
const pluginCount = createMemo(() => plugins().length)
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
const [pluginDraft, setPluginDraft] = createSignal("")
const [pluginLoading, setPluginLoading] = createSignal(false)
const overallHealthy = createMemo(() => {
const serverHealthy = server.healthy() === true
const anyMcpIssue = mcpNames().some((name) => {
Expand All @@ -200,6 +203,42 @@ export function StatusPopover() {
return serverHealthy && !anyMcpIssue
})

const savePlugins = async (plugin: string[]) => {
if (pluginLoading()) return
setPluginLoading(true)
return sdk.client.config
.update({
config: {
plugin,
},
})
.then((result) => {
if (result.data) sync.set("config", result.data)
})
.catch((err) =>
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
}),
)
.finally(() => setPluginLoading(false))
}

const addPlugin = () => {
const value = pluginDraft().trim()
if (!value) return
const list = Array.from(new Set([...plugins(), value]))
if (list.length === plugins().length) {
setPluginDraft("")
return
}
setPluginDraft("")
return savePlugins(list)
}

const removePlugin = (value: string) => savePlugins(plugins().filter((item) => item !== value))

return (
<Popover
open={shown()}
Expand Down Expand Up @@ -400,6 +439,31 @@ export function StatusPopover() {
<Tabs.Content value="plugins">
<div class="flex flex-col px-2 pb-2">
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
<div class="flex items-center gap-2 px-2 py-1 border-b border-border-weak-base mb-2">
<input
type="text"
value={pluginDraft()}
onInput={(event) => setPluginDraft(event.currentTarget.value)}
placeholder="plugin package or file:// URL"
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
class="flex-1 bg-transparent text-14-regular text-text-base outline-none placeholder:text-text-weak"
onKeyDown={(event: KeyboardEvent) => {
if (event.key !== "Enter") return
event.preventDefault()
void addPlugin()
}}
/>
<IconButton
icon="plus"
variant="ghost"
aria-label={language.t("common.save")}
disabled={pluginLoading()}
onClick={() => void addPlugin()}
/>
</div>
<Show
when={plugins().length > 0}
fallback={<div class="text-14-regular text-text-base text-center my-auto">{pluginEmpty()}</div>}
Expand All @@ -408,7 +472,14 @@ export function StatusPopover() {
{(plugin) => (
<div class="flex items-center gap-2 w-full px-2 py-1">
<div class="size-1.5 rounded-full shrink-0 bg-icon-success-base" />
<span class="text-14-regular text-text-base truncate">{plugin}</span>
<span class="text-14-regular text-text-base truncate flex-1">{plugin}</span>
<IconButton
icon="close"
variant="ghost"
aria-label={language.t("common.delete")}
disabled={pluginLoading()}
onClick={() => void removePlugin(plugin)}
/>
</div>
)}
</For>
Expand Down
Loading