feat(plugin): expose app metadata (#38179)

This commit is contained in:
Dax 2026-07-21 18:16:25 -04:00 committed by GitHub
commit 8de40be6ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 477 additions and 286 deletions

View file

@ -32,10 +32,12 @@ import {
} from "solid-js"
import {
TuiLifecycleProvider,
TuiAppProvider,
TuiPathsProvider,
TuiStartupProvider,
TuiTerminalEnvironmentProvider,
useTuiStartup,
type TuiApp,
} from "./context/runtime"
import { DialogProvider, useDialog } from "./ui/dialog"
import { DialogIntegration } from "./component/dialog-integration"
@ -139,6 +141,7 @@ const appBindingCommands = [
] as const
export type TuiInput = {
app: TuiApp
server: {
endpoint: Endpoint
service?: {
@ -224,7 +227,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
}
if (process.env.OPENCODE_DRIVE) {
const { Drive } = yield* Effect.promise(() => import("@opencode-ai/simulation/frontend"))
return yield* Drive.create(options)
return yield* Drive.create(options, input.app.version)
}
return yield* Effect.acquireRelease(
Effect.tryPromise({
@ -273,9 +276,10 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
}}
>
<EpilogueProvider set={(value) => (exit.epilogue = value)}>
<ErrorBoundary
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} mode={mode} />}
>
<TuiAppProvider value={input.app}>
<ErrorBoundary
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} mode={mode} />}
>
<TuiPathsProvider
value={{
cwd: process.cwd(),
@ -381,7 +385,8 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
</TuiTerminalEnvironmentProvider>
</TuiLifecycleProvider>
</TuiPathsProvider>
</ErrorBoundary>
</ErrorBoundary>
</TuiAppProvider>
</EpilogueProvider>
</ExitProvider>
</LogProvider>

View file

@ -1,6 +1,5 @@
import { TextAttributes } from "@opentui/core"
import { createMemo, createSignal, For } from "solid-js"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/util/installation/version"
import { Keymap } from "../context/keymap"
import { useTheme } from "../context/theme"
import { useDialog } from "../ui/dialog"
@ -9,6 +8,7 @@ import { useLocal } from "../context/local"
import { useClipboard } from "../context/clipboard"
import { useToast } from "../ui/toast"
import { describeOS, describeTerminal } from "../util/system"
import { useTuiApp } from "../context/runtime"
export function DialogDebug() {
const { themeV2 } = useTheme()
@ -17,6 +17,7 @@ export function DialogDebug() {
const local = useLocal()
const clipboard = useClipboard()
const toast = useToast()
const app = useTuiApp()
const [copied, setCopied] = createSignal(false)
dialog.setSize("large")
@ -24,7 +25,7 @@ export function DialogDebug() {
const entries = createMemo(() => {
const model = local.model.current()
return [
{ label: "Version", value: `${InstallationVersion} (${InstallationChannel})` },
{ label: "Version", value: `${app.version} (${app.channel})` },
{ label: "Date", value: new Date().toISOString() },
{ label: "OS", value: describeOS() },
{ label: "Terminal", value: describeTerminal() },

View file

@ -3,14 +3,15 @@ import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import { createSignal, For, Show } from "solid-js"
import { getScrollAcceleration } from "../util/scroll"
import { useClipboard } from "../context/clipboard"
import { InstallationVersion } from "@opencode-ai/util/installation/version"
import { useExit } from "../context/exit"
import { useTuiApp } from "../context/runtime"
import { describeOS, describeTerminal } from "../util/system"
export function ErrorComponent(props: { error: Error; reset: () => void; mode?: "dark" | "light" }) {
const term = useTerminalDimensions()
const exit = useExit()
const clipboard = useClipboard()
const app = useTuiApp()
const [copied, setCopied] = createSignal(false)
// Safe fallback palette per mode (mirrors theme/assets/opencode.json) since the
@ -42,7 +43,7 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
const message = props.error.message || "An unknown error occurred."
const stack = props.error.stack || "No stack trace available."
const issueURL = buildIssueURL(message, stack)
const issueURL = buildIssueURL(message, stack, app.version)
const copyReport = () => {
void clipboard.write?.(issueURL.toString()).then(() => setCopied(true))
@ -192,7 +193,7 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
? "Report copied — paste it into a new GitHub issue."
: "Copy the report and open a GitHub issue to help us fix this."}
</text>
<text fg={colors.muted}>OpenCode {InstallationVersion}</text>
<text fg={colors.muted}>OpenCode {app.version}</text>
</box>
</Show>
</box>
@ -200,13 +201,13 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
)
}
function buildIssueURL(message: string, stack: string) {
function buildIssueURL(message: string, stack: string, version: string) {
// Field keys match the ids in .github/ISSUE_TEMPLATE/bug-report.yml so the issue
// form opens pre-filled. Populating os/terminal/reproduce keeps the report past
// the contributing-guidelines compliance check, which pushes for system info.
const url = new URL("https://github.com/anomalyco/opencode/issues/new?template=bug-report.yml")
url.searchParams.set("title", `TUI crash: ${message}`)
url.searchParams.set("opencode-version", InstallationVersion)
url.searchParams.set("opencode-version", version)
url.searchParams.set("os", describeOS())
url.searchParams.set("terminal", describeTerminal())
url.searchParams.set(

View file

@ -1,5 +1,11 @@
import { createComponent, createContext, type JSX, useContext } from "solid-js"
export type TuiApp = Readonly<{
name: string
version: string
channel: string
}>
export type TuiPaths = Readonly<{
cwd: string
home: string
@ -23,6 +29,7 @@ export type TuiLifecycle = Readonly<{
}>
const PathsContext = createContext<TuiPaths>()
const AppContext = createContext<TuiApp>()
const TerminalEnvironmentContext = createContext<TuiTerminalEnvironment>()
const StartupContext = createContext<TuiStartup>()
const LifecycleContext = createContext<TuiLifecycle>()
@ -40,6 +47,10 @@ export function TuiPathsProvider(props: { value: TuiPaths; children: JSX.Element
return provider(PathsContext, props.value, () => props.children)
}
export function TuiAppProvider(props: { value: TuiApp; children: JSX.Element }) {
return provider(AppContext, props.value, () => props.children)
}
export function TuiTerminalEnvironmentProvider(props: { value: TuiTerminalEnvironment; children: JSX.Element }) {
return provider(TerminalEnvironmentContext, props.value, () => props.children)
}
@ -62,6 +73,10 @@ export function useTuiPaths() {
return required(PathsContext, "TuiPathsProvider")
}
export function useTuiApp() {
return required(AppContext, "TuiAppProvider")
}
export function useTuiTerminalEnvironment() {
return required(TerminalEnvironmentContext, "TuiTerminalEnvironmentProvider")
}

View file

@ -1,8 +1,7 @@
import { Plugin } from "@opencode-ai/plugin/v2/tui"
import { InstallationVersion } from "@opencode-ai/util/installation/version"
import { createMemo, Match, Show, Switch } from "solid-js"
import { useTerminalDimensions } from "@opentui/solid"
import { useTuiPaths } from "../../context/runtime"
import { useTuiApp, useTuiPaths } from "../../context/runtime"
import { useTheme } from "../../context/theme"
import { abbreviateHome } from "../../runtime"
import { FilePath } from "../../ui/file-path"
@ -50,6 +49,7 @@ function Mcp(props: { context: Plugin.Context }) {
function View(props: { context: Plugin.Context }) {
const { themeV2 } = useTheme()
const app = useTuiApp()
const dimensions = useTerminalDimensions()
const mcpWidth = createMemo(() => {
const list = props.context.data.location.mcp.server.list(props.context.location) ?? []
@ -71,12 +71,12 @@ function View(props: { context: Plugin.Context }) {
>
<Directory
context={props.context}
maxWidth={Math.max(2, dimensions().width - 8 - stringWidth(InstallationVersion) - mcpWidth())}
maxWidth={Math.max(2, dimensions().width - 8 - stringWidth(app.version) - mcpWidth())}
/>
<Mcp context={props.context} />
<box flexGrow={1} />
<box flexShrink={0}>
<text fg={themeV2.text.subdued}>{InstallationVersion}</text>
<text fg={themeV2.text.subdued}>{app.version}</text>
</box>
</box>
)