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
506 changes: 274 additions & 232 deletions packages/opencode/src/acp/agent.ts

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions packages/opencode/src/bus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,19 @@ export namespace Bus {
match.splice(index, 1)
}
}

/** @internal For testing purposes only - returns subscription count for a given event type */
export function _getSubscriptionCount(type: string): number {
const subs = state().subscriptions.get(type)
return subs?.length ?? 0
}

/** @internal For testing purposes only - returns total subscription count across all event types */
export function _getTotalSubscriptionCount(): number {
let total = 0
for (const subs of state().subscriptions.values()) {
total += subs.length
}
return total
}
}
77 changes: 50 additions & 27 deletions packages/opencode/src/format/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ export namespace Format {
}
})

// Separate state for subscriptions with dispose callback
const subscriptionState = Instance.state<(() => void)[]>(
() => [],
async () => {
// Delegate to the exported dispose function to keep cleanup logic centralized
dispose()
},
)

async function isEnabled(item: Formatter.Info) {
const s = await state()
let status = s.enabled[item.name]
Expand Down Expand Up @@ -102,36 +111,50 @@ export namespace Format {

export function init() {
log.info("init")
Bus.subscribe(File.Event.Edited, async (payload) => {
const file = payload.properties.file
log.info("formatting", { file })
const ext = path.extname(file)

for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
try {
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: Instance.directory,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
// Clean up any existing subscriptions to prevent duplicates on re-init
dispose()
const subscriptions = subscriptionState()
subscriptions.push(
Bus.subscribe(File.Event.Edited, async (payload) => {
const file = payload.properties.file
log.info("formatting", { file })
const ext = path.extname(file)

for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
try {
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: Instance.directory,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
command: item.command,
...item.environment,
})
} catch (error) {
log.error("failed to format file", {
error,
command: item.command,
...item.environment,
file,
})
} catch (error) {
log.error("failed to format file", {
error,
command: item.command,
...item.environment,
file,
})
}
}
}
})
}),
)
}

export function dispose() {
const subscriptions = subscriptionState()
for (const unsub of subscriptions) {
unsub()
}
subscriptions.length = 0
log.info("disposed format subscriptions")
}
}
39 changes: 31 additions & 8 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ export namespace Plugin {
}
})

// Separate state for subscriptions with dispose callback
const subscriptionState = Instance.state<(() => void)[]>(
() => [],
async () => {
// Delegate to the exported dispose function to keep cleanup logic centralized
dispose()
},
)

export async function trigger<
Name extends Exclude<keyof Required<Hooks>, "auth" | "event" | "tool">,
Input = Parameters<Required<Hooks>[Name]>[0],
Expand All @@ -103,19 +112,33 @@ export namespace Plugin {
}

export async function init() {
// Clean up any existing subscriptions to prevent duplicates on re-init
dispose()
const subscriptions = subscriptionState()
const hooks = await state().then((x) => x.hooks)
const config = await Config.get()
for (const hook of hooks) {
// @ts-expect-error this is because we haven't moved plugin to sdk v2
await hook.config?.(config)
}
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
hook["event"]?.({
event: input,
})
}
})
subscriptions.push(
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
hook["event"]?.({
event: input,
})
}
}),
)
}

export function dispose() {
const subscriptions = subscriptionState()
for (const unsub of subscriptions) {
unsub()
}
subscriptions.length = 0
log.info("disposed plugin subscriptions")
}
}
Loading