Compare commits

..

1 Commits

Author SHA1 Message Date
Kit Langton e0608e74ac refactor(core): use Latch for plugin supervisor ready gate 2026-08-19 23:44:58 -04:00
4 changed files with 34 additions and 95 deletions
+5 -5
View File
@@ -3,7 +3,7 @@ export { Service, type Interface } from "./supervisor-service.js"
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin"
import { Event } from "@opencode-ai/schema/config"
import { Cause, Deferred, Effect, Layer, Schema, Stream } from "effect"
import { Cause, Effect, Latch, Layer, Schema, Stream } from "effect"
import path from "path"
import { pathToFileURL } from "url"
import { ConfigPluginSource } from "../config/plugin/source.js"
@@ -137,7 +137,7 @@ export const layer = Layer.effect(
const sdk = yield* SdkPlugins.Service
const sources = yield* ConfigPluginSource.Service
const bus = yield* Bus.Service
const ready = { current: yield* Deferred.make<void>() }
const ready = yield* Latch.make()
let observed = 0
const activate = Effect.fn("PluginSupervisor.activate")(function* () {
@@ -164,7 +164,7 @@ export const layer = Layer.effect(
Stream.mapEffect(() =>
Effect.gen(function* () {
observed++
if (yield* Deferred.isDone(ready.current)) ready.current = yield* Deferred.make<void>()
yield* ready.close
return observed
}),
),
@@ -176,12 +176,12 @@ export const layer = Layer.effect(
Stream.runForEach((target) =>
Effect.gen(function* () {
yield* activate()
if (observed === target) yield* Deferred.succeed(ready.current, undefined)
if (observed === target) yield* ready.open
}).pipe(Effect.catchCause((cause) => Effect.logError("failed to reload plugins", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
return Service.of({ flush: Effect.suspend(() => Deferred.await(ready.current)) })
return Service.of({ flush: ready.await })
}),
)
+28 -55
View File
@@ -1629,11 +1629,10 @@ function SessionReasoningGroupView(props: {
const message = props.message(ref.messageID)
if (message?.type !== "assistant") return []
const part = resolvePart(message, ref.partID)
if (part?.type !== "reasoning" || (!reasoningContent(part) && !part.state)) return []
if (part?.type !== "reasoning" || !reasoningContent(part)) return []
return [{ message, part }]
}),
)
const opaque = createMemo(() => parts().length > 0 && parts().every((item) => !reasoningContent(item.part)))
const latest = createMemo((previous: string | null) => {
const item = parts().at(-1)
if (!item) return previous
@@ -1658,7 +1657,7 @@ function SessionReasoningGroupView(props: {
>
<box flexDirection="column" flexShrink={0}>
<InlineToolRow
icon={opaque() ? "" : expanded() ? "-" : "+"}
icon={expanded() ? "-" : "+"}
color={
!props.completed
? theme.text.default
@@ -1674,29 +1673,19 @@ function SessionReasoningGroupView(props: {
complete={props.completed}
pending={latest() ? `Thinking: ${latest()}` : "Thinking"}
spinner={!props.completed}
onMouseOver={() => !opaque() && setHover(true)}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText() || opaque()) return
if (renderer.getSelection()?.getSelectedText()) return
setExpanded((value) => !value)
}}
>
<Show
when={opaque() && props.completed}
fallback={
<>
{props.completed ? "Thought" : latest() ? `Thinking: ${latest()}` : "Thinking"}
<Show when={props.completed && !expanded() && latest()}>: {latest()}</Show>
<Show when={props.completed && parts().length > 1}> · {parts().length} steps</Show>
<Show when={props.completed && duration()}> · {Locale.duration(duration())}</Show>
</>
}
>
Thought
<Show when={duration()}> · {Locale.duration(duration())}</Show> · encrypted
</Show>
{props.completed ? "Thought" : latest() ? `Thinking: ${latest()}` : "Thinking"}
<Show when={props.completed && !expanded() && latest()}>: {latest()}</Show>
<Show when={props.completed && parts().length > 1}> · {parts().length} steps</Show>
<Show when={props.completed && duration()}> · {Locale.duration(duration())}</Show>
</InlineToolRow>
<Show when={expanded() && !opaque()}>
<Show when={expanded()}>
<box paddingLeft={3}>
<For each={props.refs}>
{(ref) => {
@@ -2307,7 +2296,6 @@ function ReasoningPart(props: {
const [expanded, setExpanded] = createSignal(false)
const content = createMemo(() => reasoningContent(props.part))
const opaque = createMemo(() => !content() && Boolean(props.part.state))
const isDone = createMemo(
() => props.part.time?.completed !== undefined || props.message.time.completed !== undefined,
)
@@ -2319,12 +2307,12 @@ function ReasoningPart(props: {
})
const summary = createMemo(() => reasoningSummary(content()))
const toggle = () => {
if (!inMinimal() || opaque()) return
if (!inMinimal()) return
setExpanded((prev) => !prev)
}
return (
<Show when={content() || opaque()}>
<Show when={content()}>
<box paddingLeft={3} flexDirection="column" flexShrink={0}>
<box
border={!inMinimal() || expanded() ? ["left"] : undefined}
@@ -2334,16 +2322,15 @@ function ReasoningPart(props: {
>
<box onMouseUp={toggle}>
<ReasoningHeader
toggleable={inMinimal() && !opaque()}
toggleable={inMinimal()}
open={!inMinimal() || expanded()}
done={isDone()}
title={inMinimal() && !expanded() ? summary().title : null}
duration={isDone() ? Locale.duration(duration()) : undefined}
encrypted={opaque()}
/>
</box>
</box>
<Show when={!opaque() && (!inMinimal() || expanded())}>
<Show when={!inMinimal() || expanded()}>
<box marginTop={1}>
<box
border={["left"]}
@@ -2379,7 +2366,6 @@ function ReasoningHeader(props: {
done: boolean
title: string | null
duration?: string
encrypted?: boolean
}) {
const theme = useTheme()
const fg = () =>
@@ -2401,34 +2387,21 @@ function ReasoningHeader(props: {
</Match>
<Match when={true}>
<text fg={fg()} wrapMode="none">
<Show
when={props.encrypted}
fallback={
<>
<Show when={props.toggleable}>
<span>{props.open ? "- " : "+ "}</span>
</Show>
<span>Thought</span>
<Show when={props.title || props.duration}>
<span>: </span>
</Show>
<Show when={props.title}>
<span>{props.title}</span>
</Show>
<Show when={props.duration}>
<span>
{props.title ? " · " : ""}
{props.duration}
</span>
</Show>
</>
}
>
<span>Thought</span>
<Show when={props.duration}>
<span> · {props.duration}</span>
</Show>
<span> · encrypted</span>
<Show when={props.toggleable}>
<span>{props.open ? "- " : "+ "}</span>
</Show>
<span>Thought</span>
<Show when={props.title || props.duration}>
<span>: </span>
</Show>
<Show when={props.title}>
<span>{props.title}</span>
</Show>
<Show when={props.duration}>
<span>
{props.title ? " · " : ""}
{props.duration}
</span>
</Show>
</text>
</Match>
+1 -13
View File
@@ -239,13 +239,6 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
if (event.data.sessionID === sessionID() && event.data.text.trim())
appendPart({ messageID: event.data.assistantMessageID, partID: `text:${event.data.ordinal}` }, { type: "text" })
}),
data.on("session.reasoning.started", (event) => {
if (event.data.sessionID === sessionID())
appendPart(
{ messageID: event.data.assistantMessageID, partID: `reasoning:${event.data.ordinal}` },
{ type: "reasoning" },
)
}),
data.on("session.reasoning.delta", (event) => {
if (event.data.sessionID === sessionID() && event.data.delta.trim())
appendPart(
@@ -312,12 +305,7 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
const ordinals = { text: 0, reasoning: 0 }
message.content.forEach((part) => {
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
if (
(part.type === "text" || part.type === "reasoning") &&
!part.text.trim() &&
!(part.type === "reasoning" && part.state)
)
return
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
append(rows, { messageID: message.id, partID }, part)
})
const terminal = (message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error
@@ -266,28 +266,6 @@ test("groups across empty assistant reasoning parts", () => {
])
})
test("keeps empty reasoning with provider state", () => {
const message = assistant("assistant-1", [
{
type: "reasoning",
text: "",
state: { reasoningEncryptedContent: "opaque" },
time: { created: 1_000, completed: 4_200 },
},
])
message.finish = "stop"
expect(reduceSessionRows([message])).toEqual([
{
type: "group",
kind: "reasoning",
completed: true,
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
},
{ type: "assistant-footer", messageID: "assistant-1" },
])
})
test("completes exploration groups when another row follows", () => {
const finished = assistant("assistant-2", [
{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } },