mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-06 18:07:20 +00:00
feat(tui): polish plugin dialog sizing, actions, and local footer
This commit is contained in:
parent
962c26bdf2
commit
2b87169cc1
2 changed files with 38 additions and 36 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import type { PluginInfo } from "@opencode-ai/client"
|
||||
import { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import path from "path"
|
||||
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
|
||||
import { DialogErrorDetails } from "../../component/dialog-error-details"
|
||||
import { Spinner } from "../../component/spinner"
|
||||
|
|
@ -37,7 +38,7 @@ export function PluginsDialog(props: {
|
|||
() => (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"))
|
||||
onMount(() => dialog.setSize("large"))
|
||||
onCleanup(props.context.data.on("plugin.updated", () => void refetch()))
|
||||
const updating = (entry: Entry) =>
|
||||
pending().includes(entry.key) ||
|
||||
|
|
@ -167,13 +168,6 @@ export function PluginsDialog(props: {
|
|||
.check({ location: props.context.location ?? props.context.data.location.default() })
|
||||
.then((result) => {
|
||||
mutate(result.data)
|
||||
const count = result.data.filter(
|
||||
(plugin) => plugin.source.type === "package" && plugin.source.outdated === true,
|
||||
).length
|
||||
props.context.ui.toast.show({
|
||||
variant: count ? "info" : "success",
|
||||
message: count ? `${count} plugin update${count === 1 ? "" : "s"} available` : "All plugins are up to date",
|
||||
})
|
||||
})
|
||||
.catch((cause) => {
|
||||
props.context.ui.toast.show({
|
||||
|
|
@ -218,19 +212,7 @@ export function PluginsDialog(props: {
|
|||
}}
|
||||
actions={[
|
||||
{
|
||||
title: toggleTitle(),
|
||||
command: "plugins.toggle",
|
||||
hidden: !focusedTui(),
|
||||
onTrigger: (option) => toggle(entries().find((entry) => entry.key === option.value)),
|
||||
},
|
||||
{
|
||||
title: "update",
|
||||
command: "dialog.plugins.update",
|
||||
hidden: !updatable(focusedEntry()),
|
||||
onTrigger: (option) => update(entries().find((entry) => entry.key === option.value)),
|
||||
},
|
||||
{
|
||||
title: checking() ? "checking" : "check",
|
||||
title: checking() ? "checking for updates" : "check for updates",
|
||||
command: "dialog.plugins.check",
|
||||
selection: "none",
|
||||
hidden: !entries().some(
|
||||
|
|
@ -239,6 +221,20 @@ export function PluginsDialog(props: {
|
|||
disabled: checking(),
|
||||
onTrigger: check,
|
||||
},
|
||||
{
|
||||
title: toggleTitle(),
|
||||
command: "plugins.toggle",
|
||||
side: "right",
|
||||
hidden: !focusedTui(),
|
||||
onTrigger: (option) => toggle(entries().find((entry) => entry.key === option.value)),
|
||||
},
|
||||
{
|
||||
title: "update",
|
||||
command: "dialog.plugins.update",
|
||||
side: "right",
|
||||
hidden: !updatable(focusedEntry()),
|
||||
onTrigger: (option) => update(entries().find((entry) => entry.key === option.value)),
|
||||
},
|
||||
]}
|
||||
footer={
|
||||
<Show when={pluginError(focusedEntry())}>
|
||||
|
|
@ -262,7 +258,7 @@ export function PluginsDialog(props: {
|
|||
context={`Plugin: ${label(entry(), props.context)}\nStatus: failed\nRuntime: ${entry().runtime}\nSource: ${pluginSource(entry(), props.context)}`}
|
||||
onBack={() => {
|
||||
setDetail()
|
||||
dialog.setSize("medium")
|
||||
dialog.setSize("large")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -287,6 +283,14 @@ function source(plugin: PluginInfo, context: Plugin.Context) {
|
|||
return plugin.source.type
|
||||
}
|
||||
|
||||
function isLocal(entry: Entry) {
|
||||
if (entry.runtime === "server") return entry.plugin.source.type === "local"
|
||||
const target = entry.target
|
||||
return (
|
||||
target.startsWith("file://") || target.startsWith("./") || target.startsWith("../") || path.isAbsolute(target)
|
||||
)
|
||||
}
|
||||
|
||||
function status(entry: Entry) {
|
||||
if (entry.runtime === "server") return entry.plugin.state.status
|
||||
return entry.status
|
||||
|
|
@ -299,6 +303,7 @@ function outdated(entry: Entry) {
|
|||
function footer(entry: Entry) {
|
||||
const details = [
|
||||
...(status(entry) === "active" ? [] : [status(entry)]),
|
||||
...(isLocal(entry) ? ["local"] : []),
|
||||
...(entry.runtime === "server" && entry.plugin.source.type === "package" && entry.plugin.source.version
|
||||
? [displayVersion(entry.plugin.source.version)]
|
||||
: []),
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ async function renderPlugins(root: string, inventory: { list: PluginInfo[]; chec
|
|||
|
||||
const app = await testRender(() => <Harness />, { width: 80, height: 20, kittyKeyboard: true })
|
||||
app.renderer.start()
|
||||
await app.waitForFrame((frame) => frame.includes("Plugins"))
|
||||
await app.waitForFrame((frame) => frame.includes("team.plugins") || frame.includes("local.plugin"))
|
||||
return { app, requests, toasts }
|
||||
}
|
||||
|
||||
|
|
@ -102,16 +102,16 @@ test("checking for updates refreshes the inventory and reveals the update action
|
|||
const fixture = await renderPlugins(tmp.path, { list: [packagePlugin(false)], check: [packagePlugin(true)] })
|
||||
|
||||
try {
|
||||
const initial = await fixture.app.waitForFrame((frame) => frame.includes("dadba13"))
|
||||
expect(initial).toContain("check ctrl+r")
|
||||
expect(initial).not.toContain("update available")
|
||||
expect(initial).not.toContain("ctrl+u")
|
||||
// The update action starts hidden: triggering it before a check issues no request.
|
||||
fixture.app.mockInput.pressKey("u", { ctrl: true })
|
||||
await fixture.app.flush()
|
||||
expect(fixture.requests).toEqual([])
|
||||
|
||||
fixture.app.mockInput.pressKey("r", { ctrl: true })
|
||||
const checked = await fixture.app.waitForFrame((frame) => frame.includes("update available"))
|
||||
expect(checked).toContain("ctrl+u")
|
||||
await fixture.app.waitFor(() => fixture.requests.length === 1)
|
||||
expect(fixture.requests).toEqual([{ path: "/api/plugin/check", body: {} }])
|
||||
expect(fixture.toasts).toEqual([{ variant: "info", message: "1 plugin update available" }])
|
||||
// Let the check response apply before triggering the now-enabled update.
|
||||
await fixture.app.flush()
|
||||
|
||||
fixture.app.mockInput.pressKey("u", { ctrl: true })
|
||||
await fixture.app.waitFor(() => fixture.requests.length === 2)
|
||||
|
|
@ -126,12 +126,12 @@ test("checking for updates reports an up-to-date inventory", async () => {
|
|||
const fixture = await renderPlugins(tmp.path, { list: [packagePlugin(false)], check: [packagePlugin(false)] })
|
||||
|
||||
try {
|
||||
await fixture.app.waitForFrame((frame) => frame.includes("dadba13"))
|
||||
fixture.app.mockInput.pressKey("r", { ctrl: true })
|
||||
await fixture.app.waitFor(() => fixture.toasts.length === 1)
|
||||
await fixture.app.waitFor(() => fixture.requests.length === 1)
|
||||
await fixture.app.flush()
|
||||
|
||||
expect(fixture.toasts).toEqual([{ variant: "success", message: "All plugins are up to date" }])
|
||||
expect(fixture.requests).toEqual([{ path: "/api/plugin/check", body: {} }])
|
||||
expect(fixture.toasts).toEqual([])
|
||||
} finally {
|
||||
fixture.app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -148,9 +148,6 @@ test("the check action stays hidden without package plugins", async () => {
|
|||
const fixture = await renderPlugins(tmp.path, { list: [local], check: [] })
|
||||
|
||||
try {
|
||||
const frame = await fixture.app.waitForFrame((frame) => frame.includes("local.plugin"))
|
||||
expect(frame).not.toContain("ctrl+r")
|
||||
|
||||
fixture.app.mockInput.pressKey("r", { ctrl: true })
|
||||
await fixture.app.flush()
|
||||
expect(fixture.requests).toEqual([])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue