mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-05 10:34:59 +00:00
fix(tui): refresh plugin dialog after updates (#46911)
This commit is contained in:
parent
46515df4a3
commit
57c02cd04b
6 changed files with 19 additions and 74 deletions
|
|
@ -985,15 +985,6 @@ export type ReferenceUpdated = {
|
|||
data: {}
|
||||
}
|
||||
|
||||
export type PluginAdded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "plugin.added"
|
||||
location?: LocationRef
|
||||
data: { id: string }
|
||||
}
|
||||
|
||||
export type PluginUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
|
|
@ -2299,7 +2290,6 @@ export type V2Event =
|
|||
| ReferenceUpdated
|
||||
| PermissionAsked
|
||||
| PermissionReplied
|
||||
| PluginAdded
|
||||
| PluginUpdated
|
||||
| ProjectUpdated
|
||||
| WorktreeUpdated
|
||||
|
|
|
|||
|
|
@ -20378,44 +20378,6 @@
|
|||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"plugin.added": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["plugin.added"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"plugin.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -23123,9 +23085,6 @@
|
|||
{
|
||||
"$ref": "#/components/schemas/permission.v2.replied"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/plugin.added"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/plugin.updated"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ const layer = Layer.effect(
|
|||
),
|
||||
),
|
||||
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": plugin.id } }),
|
||||
Effect.andThen(bus.publish(Plugin.Event.Added, { id: Plugin.ID.make(plugin.id) })),
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||
Effect.exit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
- Preserve the dependency direction: `@opencode-ai/schema <- @opencode-ai/protocol <- @opencode-ai/server`.
|
||||
- Schema values should be serializable contract definitions, not service implementations or runtime registries.
|
||||
- A domain may keep a minimal public wire contract here when SDK generation needs it, but do not move the broader runtime model into Schema just because an event is public. `plugin.added` is the current example: Schema may own the minimum browser-safe event payload, while plugin runtime behavior stays outside Schema.
|
||||
- A domain may keep a minimal public wire contract here when SDK generation needs it, but do not move the broader runtime model into Schema just because an event is public. `plugin.updated` is the current example: Schema may own the minimum browser-safe event payload, while plugin runtime behavior stays outside Schema.
|
||||
- The root barrel exports canonical current domain contracts. Specialized event modules, manifests, infrastructure modules, and V1 contracts use direct entrypoints instead of becoming first-class root exports.
|
||||
|
||||
## Current Versus V1
|
||||
|
|
|
|||
|
|
@ -42,12 +42,8 @@ export const Info = Schema.Struct({
|
|||
state: State,
|
||||
}).annotate({ identifier: "Plugin.Info" })
|
||||
|
||||
const Added = ephemeral({
|
||||
type: "plugin.added",
|
||||
schema: { id: ID },
|
||||
})
|
||||
const Updated = ephemeral({
|
||||
type: "plugin.updated",
|
||||
schema: {},
|
||||
})
|
||||
export const Event = { Added, Updated, Definitions: inventory(Added, Updated) }
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { PluginInfo } from "@opencode-ai/client"
|
|||
import { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
|
||||
import { DialogErrorDetails } from "../../component/dialog-error-details"
|
||||
import { Spinner } from "../../component/spinner"
|
||||
import { usePlugin } from "../../plugin/context"
|
||||
import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
|
|
@ -31,12 +32,17 @@ export function PluginsDialog(props: {
|
|||
const [focused, setFocused] = createSignal<string>()
|
||||
const [detail, setDetail] = createSignal<Entry>()
|
||||
const [showInternal, setShowInternal] = createSignal(false)
|
||||
const [pending, setPending] = createSignal<readonly string[]>([])
|
||||
const [server, { refetch, mutate }] = createResource(
|
||||
() => (props.server ? undefined : (props.context.location ?? props.context.data.location.default())),
|
||||
(location) => props.context.client.plugin.list({ location }).then((result) => result.data),
|
||||
)
|
||||
onMount(() => dialog.setSize("medium"))
|
||||
onCleanup(props.context.data.on("plugin.updated", () => void refetch()))
|
||||
const updating = (entry: Entry) =>
|
||||
pending().includes(entry.key) ||
|
||||
(entry.runtime === "server" && entry.plugin.source.type === "package" && entry.plugin.source.updating === true)
|
||||
const updatable = (entry: Entry | undefined) => entry !== undefined && outdated(entry) && !updating(entry)
|
||||
const entries = createMemo<Entry[]>(() => {
|
||||
const builtins: Entry[] = props.plugins
|
||||
.registered()
|
||||
|
|
@ -86,15 +92,16 @@ export function PluginsDialog(props: {
|
|||
value: entry.key,
|
||||
category: entry.runtime === "tui" ? "TUI" : "Server",
|
||||
searchText: entry.runtime === "tui" ? entry.target : source(entry.plugin, props.context),
|
||||
footer: footer(entry),
|
||||
footer: updating(entry) ? "updating" : footer(entry),
|
||||
footerColor:
|
||||
status(entry) === "failed"
|
||||
? props.context.theme.text.feedback.error.default
|
||||
: outdated(entry)
|
||||
? props.context.theme.text.feedback.info.default
|
||||
: props.context.theme.text.subdued,
|
||||
gutter:
|
||||
status(entry) === "failed"
|
||||
gutter: updating(entry)
|
||||
? (color) => <Spinner color={color} />
|
||||
: status(entry) === "failed"
|
||||
? () => <text fg={props.context.theme.text.feedback.error.default}>x</text>
|
||||
: undefined,
|
||||
}),
|
||||
|
|
@ -131,20 +138,22 @@ export function PluginsDialog(props: {
|
|||
}
|
||||
const update = (entry: Entry | undefined) => {
|
||||
if (entry?.runtime !== "server" || entry.plugin.source.type !== "package" || !updatable(entry)) return
|
||||
const location = props.context.location ?? props.context.data.location.default()
|
||||
setPending((keys) => [...keys, entry.key])
|
||||
props.context.client.plugin
|
||||
.update({
|
||||
location: props.context.location ?? props.context.data.location.default(),
|
||||
location,
|
||||
targets: [entry.plugin.source.target],
|
||||
})
|
||||
.then(() =>
|
||||
props.context.ui.toast.show({ variant: "success", message: `Updated plugin ${label(entry, props.context)}` }),
|
||||
)
|
||||
.then(() => props.context.client.plugin.awaitActivation({ location }))
|
||||
.then(() => refetch())
|
||||
.catch((cause) => {
|
||||
props.context.ui.toast.show({
|
||||
variant: "error",
|
||||
message: cause instanceof Error ? cause.message : String(cause),
|
||||
})
|
||||
})
|
||||
.finally(() => setPending((keys) => keys.filter((key) => key !== entry.key)))
|
||||
}
|
||||
// The server only re-checks package sources on startup and then caches the
|
||||
// result for a day, so a merge pushed after launch stays invisible until the
|
||||
|
|
@ -287,21 +296,13 @@ function outdated(entry: Entry) {
|
|||
return entry.runtime === "server" && entry.plugin.source.type === "package" && entry.plugin.source.outdated === true
|
||||
}
|
||||
|
||||
function updating(entry: Entry) {
|
||||
return entry.runtime === "server" && entry.plugin.source.type === "package" && entry.plugin.source.updating === true
|
||||
}
|
||||
|
||||
function updatable(entry: Entry | undefined) {
|
||||
return entry !== undefined && outdated(entry) && !updating(entry)
|
||||
}
|
||||
|
||||
function footer(entry: Entry) {
|
||||
const details = [
|
||||
...(status(entry) === "active" ? [] : [status(entry)]),
|
||||
...(entry.runtime === "server" && entry.plugin.source.type === "package" && entry.plugin.source.version
|
||||
? [displayVersion(entry.plugin.source.version)]
|
||||
: []),
|
||||
...(updating(entry) ? ["updating"] : outdated(entry) ? ["update available"] : []),
|
||||
...(outdated(entry) ? ["update available"] : []),
|
||||
]
|
||||
return details.length ? details.join(", ") : undefined
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue