Compare commits

...

4 Commits

Author SHA1 Message Date
Kit Langton dbccc5f2c3 fix(core): broadcast connection updates to every location 2026-08-06 16:20:52 -04:00
Kit Langton c66d84169a fix(tui): open authorization links (#40912) 2026-08-06 15:26:13 -04:00
Aiden Cline 7dbe8c4c13 refactor(mcp): remove unused registration status (#40904) 2026-08-06 13:59:22 -05:00
Aiden Cline 8864b01d0b feat(core): increase retained compaction context (#40906) 2026-08-06 13:45:06 -05:00
23 changed files with 97 additions and 122 deletions
@@ -10,7 +10,6 @@ const statusLabels = {
connected: "mcp.status.connected",
failed: "mcp.status.failed",
needs_auth: "mcp.status.needs_auth",
needs_client_registration: "mcp.status.needs_client_registration",
disabled: "mcp.status.disabled",
} as const
@@ -57,7 +56,7 @@ export const DialogSelectMcp: Component = () => {
}
const error = () => {
const s = mcpStatus()
if (s?.status === "failed" || s?.status === "needs_client_registration") return s.error
if (s?.status === "failed") return s.error
}
const enabled = () => status() === "connected"
return (
@@ -426,8 +426,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
"bg-icon-success-base": status() === "connected",
"bg-icon-critical-base": status() === "failed",
"bg-border-weak-base": status() === "disabled",
"bg-icon-warning-base":
status() === "needs_auth" || status() === "needs_client_registration",
"bg-icon-warning-base": status() === "needs_auth",
}}
/>
<span class="flex flex-col min-w-0 flex-1">
@@ -35,7 +35,6 @@ describe("hasNonBlockingServiceIssue", () => {
test("detects MCP failures that do not block chatting", () => {
expect(hasNonBlockingServiceIssue({ mcp: ["failed"], lsp: [] })).toBe(true)
expect(hasNonBlockingServiceIssue({ mcp: ["needs_auth"], lsp: [] })).toBe(true)
expect(hasNonBlockingServiceIssue({ mcp: ["needs_client_registration"], lsp: [] })).toBe(true)
expect(hasNonBlockingServiceIssue({ mcp: ["connected", "pending", "disabled"], lsp: [] })).toBe(false)
})
@@ -48,7 +47,6 @@ describe("hasNonBlockingServiceIssue", () => {
describe("hasServiceNeedingAttention", () => {
test("detects MCP states that need user attention", () => {
expect(hasServiceNeedingAttention({ mcp: ["needs_auth"] })).toBe(true)
expect(hasServiceNeedingAttention({ mcp: ["needs_client_registration"] })).toBe(true)
})
test("ignores states that do not need user attention", () => {
@@ -2,7 +2,7 @@ import type { LspStatus } from "@/types"
import type { McpServer } from "@opencode-ai/client/promise"
export function hasServiceNeedingAttention(input: { mcp: Array<McpServer["status"]["status"]> }) {
return input.mcp.some((status) => status === "needs_auth" || status === "needs_client_registration")
return input.mcp.some((status) => status === "needs_auth")
}
export function hasNonBlockingServiceIssue(input: {
@@ -13,7 +13,6 @@ export async function toggleMcp(input: {
needs_auth: input.authenticate,
disabled: input.connect,
failed: input.connect,
needs_client_registration: input.connect,
}[input.status]()
await input.refresh()
}
@@ -34,7 +34,6 @@ function icon(status: McpServer["status"]) {
case "needs_auth":
return "⚠"
case "failed":
case "needs_client_registration":
return "✗"
default:
return "○"
@@ -45,8 +44,6 @@ function describe(status: McpServer["status"]) {
switch (status.status) {
case "needs_auth":
return "needs authentication"
case "needs_client_registration":
return `needs client registration: ${status.error}`
case "failed":
return `failed: ${status.error}`
default:
@@ -259,8 +259,6 @@ export type McpStatusFailed = { status: "failed"; error: string }
export type McpStatusNeedsAuth = { status: "needs_auth" }
export type McpStatusNeedsClientRegistration = { status: "needs_client_registration"; error: string }
export type McpResource = { server: string; name: string; uri: string; description?: string; mimeType?: string }
export type McpResourceTemplate = {
@@ -1261,13 +1259,7 @@ export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo
export type McpServer = {
name: string
status:
| McpStatusConnected
| McpStatusPending
| McpStatusDisabled
| McpStatusFailed
| McpStatusNeedsAuth
| McpStatusNeedsClientRegistration
status: McpStatusConnected | McpStatusPending | McpStatusDisabled | McpStatusFailed | McpStatusNeedsAuth
integrationID?: string
}
+8 -5
View File
@@ -416,11 +416,14 @@ export function configured(options?: Options) {
function publish<D extends Event.Definition>(definition: D, data: Event.Data<D>, options?: PublishOptions) {
return Effect.gen(function* () {
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
const location =
options?.location ??
(serviceLocation
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined)
// Global definitions describe location-independent facts. Never tag
// them, so location-filtered subscribers in every location observe them.
const location = definition.global
? undefined
: (options?.location ??
(serviceLocation
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined))
return yield* publishEvent(
definition,
{
+1 -1
View File
@@ -20,7 +20,7 @@ import type { Info } from "../model"
import { SessionUsage } from "./usage"
const DEFAULT_BUFFER = 20_000
const DEFAULT_KEEP_TOKENS = 8_000
const DEFAULT_KEEP_TOKENS = 15_000
const OUTPUT_TOKEN_MAX = 32_000
const TOOL_OUTPUT_MAX_CHARS = 2_000
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
+32
View File
@@ -75,6 +75,13 @@ const CountMessage = Bus.ephemeral({
count: Schema.Number,
},
})
const GlobalFact = Bus.ephemeral({
type: "test.global.fact",
global: true,
schema: {
text: Schema.String,
},
})
const VersionedMessage = Bus.durable({
type: "test.versioned",
@@ -153,6 +160,31 @@ describe("Bus", () => {
}),
)
it.effect("publishes global definitions untagged so subscribers in other locations observe them", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const elsewhere = Location.Service.of(
location({ directory: AbsolutePath.make("elsewhere"), workspaceID: Workspace.ID.make("wrk_other") }),
)
const fiber = yield* bus
.subscribe([Message, GlobalFact])
.pipe(
Stream.take(1),
Stream.runCollect,
Effect.provideService(Location.Service, elsewhere),
Effect.forkScoped,
)
yield* Effect.yieldNow
// Location-tagged events stay invisible to other locations; the global fact reaches them.
yield* bus.publish(Message, { text: "tagged" })
const event = yield* bus.publish(GlobalFact, { text: "everywhere" })
expect(event).not.toHaveProperty("location")
expect(Array.from(yield* Fiber.join(fiber))).toEqual([event])
}),
)
itWithoutLocation.effect("omits location when no location is available", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
-22
View File
@@ -20687,25 +20687,6 @@
],
"additionalProperties": false
},
"Mcp.Status.NeedsClientRegistration": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"needs_client_registration"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
],
"additionalProperties": false
},
"Mcp.Server": {
"type": "object",
"properties": {
@@ -20728,9 +20709,6 @@
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsAuth"
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration"
}
]
},
+6 -1
View File
@@ -37,6 +37,7 @@ export type DurableDefinition<
readonly version: number
readonly aggregate: string
}
readonly global?: never
readonly data: DataSchema
}
@@ -47,6 +48,8 @@ export type EphemeralDefinition<
readonly type: Type
readonly durability: "ephemeral"
readonly durable?: never
/** Global events describe location-independent facts: they are published untagged and reach every location. */
readonly global?: boolean
readonly data: DataSchema
}
@@ -77,13 +80,14 @@ type Input<Type extends string, Fields extends Readonly<Record<PropertyKey, Sche
readonly version: number
readonly aggregate: string
}
readonly global?: boolean
readonly schema: Fields
}
export function durable<
const Type extends string,
const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
>(input: Input<Type, Fields> & { readonly durable: NonNullable<Input<Type, Fields>["durable"]> }) {
>(input: Omit<Input<Type, Fields>, "global"> & { readonly durable: NonNullable<Input<Type, Fields>["durable"]> }) {
const data = Schema.Struct(input.schema)
const durable = Schema.Struct({
aggregateID: DurableEnvelope.fields.aggregateID,
@@ -137,6 +141,7 @@ export function ephemeral<
type: input.type,
durability: "ephemeral" as const,
durable: undefined,
global: input.global === true,
data,
})),
) satisfies EphemeralDefinition<Type, typeof data>
+4
View File
@@ -88,8 +88,12 @@ const Updated = ephemeral({
type: "integration.updated",
schema: {},
})
// Credentials live in one global store shared by every location, so a
// connection change is a location-independent fact: publish it globally so
// every active location refreshes its provider catalog.
const ConnectionUpdated = ephemeral({
type: "integration.connection.updated",
global: true,
schema: { integrationID: ID },
})
export const Event = { Updated, ConnectionUpdated, Definitions: inventory(Updated, ConnectionUpdated) }
+1 -5
View File
@@ -68,13 +68,9 @@ const Failed = Schema.Struct({ status: Schema.Literal("failed"), error: Schema.S
const NeedsAuth = Schema.Struct({ status: Schema.Literal("needs_auth") }).annotate({
identifier: "Mcp.Status.NeedsAuth",
})
const NeedsClientRegistration = Schema.Struct({
status: Schema.Literal("needs_client_registration"),
error: Schema.String,
}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
export type Status = typeof Status.Type
export const Status = Schema.Union([Connected, Pending, Disabled, Failed, NeedsAuth, NeedsClientRegistration]).pipe(
export const Status = Schema.Union([Connected, Pending, Disabled, Failed, NeedsAuth]).pipe(
Schema.toTaggedUnion("status"),
)
@@ -6,6 +6,7 @@ import type {
IntegrationOauthConnectOutput,
IntegrationOAuthMethod,
} from "@opencode-ai/client"
import open from "open"
import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
import { useClipboard } from "../context/clipboard"
import { useData } from "../context/data"
@@ -445,6 +446,19 @@ function OAuthAuto(props: {
Keymap.createLayer(() => ({
mode: "modal",
commands: [
{
bind: "o",
title: "Open authorization URL",
group: "Dialog",
run: () => {
open(props.attempt.url).catch(() =>
toast.show({
message: "Could not open the browser. Copy the URL and continue manually.",
variant: "error",
}),
)
},
},
{
bind: "c",
title: "Copy authorization details",
@@ -502,6 +516,7 @@ function OAuthAuto(props: {
instructions={props.attempt.instructions}
message="Waiting for authorization..."
copy
open
/>
)
}
@@ -559,7 +574,14 @@ function OAuthCode(props: {
)
}
function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) {
function OAuthView(props: {
title: string
url?: string
instructions?: string
message: string
copy?: boolean
open?: boolean
}) {
const dialog = useDialog()
const theme = useTheme("elevated")
return (
@@ -583,11 +605,18 @@ function OAuthView(props: { title: string; url?: string; instructions?: string;
)}
</Show>
<text fg={theme.text.subdued}>{props.message}</text>
<Show when={props.copy}>
<text fg={theme.text.default}>
c <span style={{ fg: theme.text.subdued }}>copy</span>
</text>
</Show>
<box flexDirection="row" gap={2}>
<Show when={props.open}>
<text fg={theme.text.default}>
o <span style={{ fg: theme.text.subdued }}>open</span>
</text>
</Show>
<Show when={props.copy}>
<text fg={theme.text.default}>
c <span style={{ fg: theme.text.subdued }}>copy</span>
</text>
</Show>
</box>
</box>
)
}
+1 -1
View File
@@ -15,7 +15,7 @@ import { useConfig } from "../config"
import { getScrollAcceleration } from "../util/scroll"
function statusError(status: McpServer["status"]) {
if (status.status === "failed" || status.status === "needs_client_registration") return status.error
if (status.status === "failed") return status.error
return undefined
}
@@ -14,7 +14,6 @@ export function DialogStatus() {
if (status === "connected") return theme.text.feedback.success.default
if (status === "failed") return theme.text.feedback.error.default
if (status === "needs_auth") return theme.text.feedback.warning.default
if (status === "needs_client_registration") return theme.text.feedback.error.default
return theme.text.subdued
}
return (
@@ -46,9 +45,6 @@ export function DialogStatus() {
<Match when={item.status.status === "failed" && item.status}>{(val) => val().error}</Match>
<Match when={item.status.status === "disabled"}>Disabled in configuration</Match>
<Match when={item.status.status === "needs_auth"}>Needs authentication</Match>
<Match when={item.status.status === "needs_client_registration" && item.status}>
{(val) => (val() as { error: string }).error}
</Match>
</Switch>
</span>
</text>
@@ -8,13 +8,7 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
const list = createMemo(() => props.context.data.location.mcp.server.list(session()?.location) ?? [])
const on = createMemo(() => list().filter((item) => item.status.status === "connected").length)
const bad = createMemo(
() =>
list().filter(
(item) =>
item.status.status === "failed" ||
item.status.status === "needs_auth" ||
item.status.status === "needs_client_registration",
).length,
() => list().filter((item) => item.status.status === "failed" || item.status.status === "needs_auth").length,
)
const dot = (status: string) => {
@@ -22,7 +16,6 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
if (status === "failed") return theme.text.feedback.error.default
if (status === "disabled") return theme.text.subdued
if (status === "needs_auth") return theme.text.feedback.warning.default
if (status === "needs_client_registration") return theme.text.feedback.error.default
return theme.text.subdued
}
@@ -65,7 +58,6 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
</Match>
<Match when={item.status.status === "disabled"}>Disabled</Match>
<Match when={item.status.status === "needs_auth"}>Needs auth</Match>
<Match when={item.status.status === "needs_client_registration"}>Needs client ID</Match>
</Switch>
</span>
</text>
+1 -1
View File
@@ -28,7 +28,7 @@ export function Link(props: LinkProps) {
open(props.href).catch(() => {})
}}
>
{displayText}
<a href={props.href}>{displayText}</a>
</text>
)
}
@@ -83,7 +83,7 @@ Add `compaction` to any [OpenCode configuration file](/config):
"auto": true,
"prune": false,
"keep": {
"tokens": 8000
"tokens": 15000
},
"buffer": 20000
}
@@ -94,7 +94,7 @@ Add `compaction` to any [OpenCode configuration file](/config):
| --- | ---: | --- |
| `auto` | `true` | Runs the preflight context-size check. It does not disable manual compaction or one-shot provider-overflow recovery. |
| `prune` | None | Accepted by the V2 schema, but currently has no runtime effect. V2 does not prune old tool outputs in place. |
| `keep.tokens` | `8000` | Approximate number of tokens from the newest serialized conversation context to retain beside the summary. |
| `keep.tokens` | `15000` | Approximate number of tokens from the newest serialized conversation context to retain beside the summary. |
| `buffer` | `20000` | Safety reserve below an explicit input limit. Without one, it is the minimum context reserve and the model output allowance wins when larger. |
`keep.tokens` and `buffer` accept non-negative integers. Larger `keep.tokens`
+1 -1
View File
@@ -329,7 +329,7 @@ Control automatic context compaction and how much recent context it preserves.
"compaction": {
"auto": true,
"keep": {
"tokens": 8000
"tokens": 15000
},
"buffer": 20000
}
-22
View File
@@ -20687,25 +20687,6 @@
],
"additionalProperties": false
},
"Mcp.Status.NeedsClientRegistration": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"needs_client_registration"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
],
"additionalProperties": false
},
"Mcp.Server": {
"type": "object",
"properties": {
@@ -20728,9 +20709,6 @@
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsAuth"
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration"
}
]
},
-22
View File
@@ -20687,25 +20687,6 @@
],
"additionalProperties": false
},
"Mcp.Status.NeedsClientRegistration": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"needs_client_registration"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
],
"additionalProperties": false
},
"Mcp.Server": {
"type": "object",
"properties": {
@@ -20728,9 +20709,6 @@
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsAuth"
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration"
}
]
},