Skip to content
Open
Show file tree
Hide file tree
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
28 changes: 24 additions & 4 deletions packages/app/src/components/status-popover-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export function StatusPopoverServerBody() {
const run = ++dialogRun
void import("./dialog-select-server").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
void dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
})
},
}}
Expand Down Expand Up @@ -258,7 +258,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const navigate = useNavigate()
const settings = useSettings()

const fail = (err: unknown) => {
const _fail = (err: unknown) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
Expand All @@ -280,8 +280,10 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const toggleMcp = useMcpToggle()
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
const mcpNames = createMemo(() => Object.keys(sync().data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
const axiNames = createMemo(() => Object.keys(sync().data.axi ?? {}).sort((a, b) => a.localeCompare(b)))
const mcpStatus = (name: string) => sync().data.mcp?.[name]?.status
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
const mcpOrAxiConnected = createMemo(() => mcpConnected() + axiNames().length)
const lspItems = createMemo(() => sync().data.lsp ?? [])
const lspCount = createMemo(() => lspItems().length)
const plugins = createMemo(() =>
Expand All @@ -308,7 +310,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
</Tabs.Trigger>
)}
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
{mcpOrAxiConnected() > 0 ? `${mcpOrAxiConnected()} ` : ""}
{language.t("status.popover.tab.mcp")}
</Tabs.Trigger>
<Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
Expand Down Expand Up @@ -377,7 +379,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const run = ++dialogRun
void import("./dialog-select-server").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
void dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
})
}}
>
Expand Down Expand Up @@ -445,6 +447,24 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
)
}}
</For>
<Show when={axiNames().length > 0}>
<div class="text-12-medium text-text-weaker px-3 pt-3 pb-1">AXI</div>
<For each={axiNames()}>
{(key) => {
const axi = () => sync().data.axi?.[key]
return (
<div class="flex items-center gap-2 w-full min-h-8 pl-3 pr-2 py-1 rounded-md text-left">
<div class="size-1.5 rounded-full shrink-0 bg-icon-info-base" />
<span class="flex flex-col min-w-0 flex-1">
<span class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-base truncate">{axi()?.name}</span>
</span>
</span>
</div>
)
}}
</For>
</Show>
</Show>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/components/status-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function StatusPopover() {
const warn = mcp.some((item) => item.status === "needs_auth")
if (failed) return "critical" as const
if (warn) return "warning" as const
return undefined
})
const serverHealthy = () => global.servers.health[server.key]?.healthy === true
const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue())
Expand Down Expand Up @@ -93,6 +94,7 @@ function DirectoryStatusPopover() {
const warn = mcp.some((item) => item.status === "needs_auth")
if (failed) return "critical" as const
if (warn) return "warning" as const
return undefined
})
const healthy = createMemo(() => serverHealth() === true && !mcpIssue())
const state = createMemo<StatusPopoverState>(() => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/context/directory-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export const createDirSyncContext = (
type Child = ReturnType<(typeof serverSync)["child"]>
type Setter = Child[1]

const current = createMemo(() => serverSync.child(directory, { mcp: true }))
const current = createMemo(() => serverSync.child(directory, { mcp: true, axi: true }))
const target = (directory?: string) => {
if (!directory || directory === directory) return current()
return serverSync.child(directory)
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/context/global-sync/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe("bootstrapDirectory", () => {
question: {},
mcp_ready: true,
mcp: {},
axi: {},
lsp_ready: true,
lsp: [],
vcs: undefined,
Expand All @@ -48,6 +49,7 @@ describe("bootstrapDirectory", () => {
directory: "/project",
scope: ServerScope.local,
mcp: false,
axi: false,
global: {
config: {} satisfies Config,
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/context/global-sync/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { State, VcsCache } from "./types"
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery } from "../server-sync"
import { loadMcpQuery, loadAxiQuery } from "../server-sync"
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import { ScopedKey, type ServerScope } from "@/utils/server-scope"

Expand Down Expand Up @@ -202,6 +202,7 @@ export async function bootstrapDirectory(input: {
directory: string
scope: ServerScope
mcp: boolean
axi: boolean
sdk: OpencodeClient
store: Store<State>
setStore: SetStoreFunction<State>
Expand Down Expand Up @@ -310,6 +311,7 @@ export async function bootstrapDirectory(input: {
),
() => Promise.resolve(input.loadSessions(input.directory)),
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.sdk))),
input.axi && (() => input.queryClient.fetchQuery(loadAxiQuery(input.scope, input.directory, input.sdk))),
() =>
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.sdk)).catch((err) => {
const project = getFilename(input.directory)
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/context/global-sync/child-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const queryOptionsApi = {
}),
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
axi: (directory: string) => ({ queryKey: [directory, "axi"], queryFn: async () => ({}) }),
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
} as unknown as QueryOptionsApi
Expand All @@ -59,6 +60,7 @@ beforeAll(async () => {
get data() {
if (options().queryKey?.[1] === "path") throw new Error("pending path data read")
if (options().queryKey?.[1] === "mcp") return options().enabled ? { demo: { status: "disabled" } } : undefined
if (options().queryKey?.[1] === "axi") return options().enabled ? { demo: { name: "demo", uri: "axi://demo" } } : undefined
if (options().queryKey?.[1] === "lsp") return []
if (options().queryKey?.[1] === "providers") return provider
return undefined
Expand Down Expand Up @@ -197,7 +199,7 @@ describe("createChildStoreManager", () => {
try {
if (!manager) throw new Error("manager required")
const [store, setStore] = manager.child("/project", { bootstrap: false })
expect(querySingles.length - offset).toBe(4)
expect(querySingles.length - offset).toBe(5)
const query = querySingles[offset + 1]
if (!query) throw new Error("query required")
expect(query().enabled).toBe(false)
Expand Down
26 changes: 26 additions & 0 deletions packages/app/src/context/global-sync/child-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export function createChildStoreManager(input: {
const disposers = new Map<string, () => void>()
const mcpDirectories = new Set<string>()
const mcpToggles = new Map<string, (enabled: boolean) => void>()
const axiDirectories = new Set<string>()
const axiToggles = new Map<string, (enabled: boolean) => void>()

const markKey = (key: DirectoryKey) => {
if (!key) return
Expand Down Expand Up @@ -118,6 +120,8 @@ export function createChildStoreManager(input: {
lifecycle.delete(key)
mcpDirectories.delete(key)
mcpToggles.delete(key)
axiDirectories.delete(key)
axiToggles.delete(key)
const dispose = disposers.get(key)
if (dispose) {
dispose()
Expand Down Expand Up @@ -182,9 +186,11 @@ export function createChildStoreManager(input: {
const initialMeta = meta[0].value
const initialIcon = icon[0].value
const [mcpEnabled, setMcpEnabled] = createSignal(false)
const [axiEnabled, setAxiEnabled] = createSignal(false)

const pathQuery = useQuery(() => input.queryOptions.path(key))
const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
const axiQuery = useQuery(() => ({ ...input.queryOptions.axi(key), enabled: axiEnabled() }))
const lspQuery = useQuery(() => input.queryOptions.lsp(key))
const providerQuery = useQuery(() => input.queryOptions.providers(key))

Expand Down Expand Up @@ -227,6 +233,9 @@ export function createChildStoreManager(input: {
get mcp() {
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
},
get axi() {
return axiQuery.isLoading ? {} : (axiQuery.data ?? {})
},
get lsp_ready() {
return !lspQuery.isLoading
},
Expand All @@ -242,6 +251,7 @@ export function createChildStoreManager(input: {
children[key] = child
disposers.set(key, dispose)
mcpToggles.set(key, setMcpEnabled)
axiToggles.set(key, setAxiEnabled)

const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
if (!(init instanceof Promise)) return
Expand Down Expand Up @@ -281,6 +291,7 @@ export function createChildStoreManager(input: {
const childStore = ensureChild(directory)
pinForOwner(key)
if (options.mcp) enableMcp(directory, key, childStore)
if (options.axi) enableAxi(directory, key)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap && childStore[0].status === "loading") {
input.onBootstrap(directory)
Expand All @@ -292,6 +303,7 @@ export function createChildStoreManager(input: {
const key = directoryKey(directory)
const childStore = ensureChild(directory)
if (options.mcp) enableMcp(directory, key, childStore)
if (options.axi) enableAxi(directory, key)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap && childStore[0].status === "loading") {
input.onBootstrap(directory)
Expand All @@ -312,6 +324,18 @@ export function createChildStoreManager(input: {
mcpToggles.get(key)?.(false)
}

function enableAxi(directory: string, key: DirectoryKey) {
if (axiDirectories.has(key)) return
axiDirectories.add(key)
axiToggles.get(key)?.(true)
}

function disableAxi(directory: string) {
const key = directoryKey(directory)
if (!axiDirectories.delete(key)) return
axiToggles.get(key)?.(false)
}

function projectMeta(directory: string, patch: ProjectMeta) {
const key = directoryKey(directory)
const [store, setStore] = ensureChild(directory)
Expand Down Expand Up @@ -352,7 +376,9 @@ export function createChildStoreManager(input: {
unpin,
pinned,
mcp: (directory: string) => mcpDirectories.has(directoryKey(directory)),
axi: (directory: string) => axiDirectories.has(directoryKey(directory)),
disableMcp,
disableAxi,
disposeDirectory,
runEviction,
vcsCache,
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/context/global-sync/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export type State = {
mcp: {
[name: string]: McpStatus
}
axi: {
[name: string]: { name: string; uri: string; description?: string; mimeType?: string }
}
lsp_ready: boolean
lsp: LspStatus[]
vcs: VcsInfo | undefined
Expand Down Expand Up @@ -99,6 +102,7 @@ export type IconCache = {
export type ChildOptions = {
bootstrap?: boolean
mcp?: boolean
axi?: boolean
}

export type DirState = {
Expand Down
26 changes: 26 additions & 0 deletions packages/app/src/context/server-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ export const loadMcpQuery = (scope: ServerScope, directory: string, sdk: Opencod
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
})

export const loadAxiQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: [scope, directory, "axi"] as const,
queryFn: async () => {
const axiEntries: Record<string, { name: string; uri: string; description?: string; mimeType?: string }> = {}
try {
const resources = await sdk.experimental.resource.list().then((r) => r.data ?? {})
for (const [key, resource] of Object.entries(resources)) {
if (resource.client === "axi") {
axiEntries[key] = {
name: resource.name,
uri: resource.uri,
description: resource.description,
mimeType: resource.mimeType,
}
}
}
} catch {
// AXI failures should not break bootstrap — return empty
}
return axiEntries
},
})

export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: [scope, directory, "lsp"] as const,
Expand All @@ -78,6 +102,7 @@ function makeQueryOptionsApi(
loadPathQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)),
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, sdkFor(directory)),
mcp: (directory: PathKey) => loadMcpQuery(scope, directory, sdkFor(directory)),
axi: (directory: PathKey) => loadAxiQuery(scope, directory, sdkFor(directory)),
lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)),
sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
}
Expand Down Expand Up @@ -344,6 +369,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
directory,
scope: serverSDK.scope,
mcp: children.mcp(key),
axi: children.axi(key),
global: {
config: globalStore.config,
path: globalStore.path,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ export const dict = {
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Server configurations",
"status.popover.tab.servers": "Servers",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.mcp": "MCP/AXI",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Manage servers",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/plugin/skill/customize-opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,9 @@ disable a server inherited from a parent config. String values such as header
tokens support `{env:VAR}` interpolation (and `{file:path}`); the shell-style
`${VAR}` is not substituted.

Local servers accept an optional `cwd` field to set the working directory for
the server process. Relative paths resolve from the workspace directory.

## Permissions

```json
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/run/footer.prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export function createPromptState(input: PromptInput): PromptState {
const resources = createMemo<Auto[]>(() => {
return input.resources().map((item) => ({
kind: "mention",
display: Locale.truncateMiddle(`@${item.name} (${item.uri})`, width()),
display: Locale.truncateMiddle(`${item.client === "axi" ? "✧ " : "◎ "}@${item.name} (${item.uri})`, width()),
value: item.name,
description: item.description,
part: {
Expand Down
Loading
Loading