Skip to content
Merged
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
59 changes: 56 additions & 3 deletions opencode-plugin/copilot-tools-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,19 @@ function normalizeToolArgs(toolName: string, args: Record<string, unknown>): Rec
return normalized
}

type SubagentInfo = {
parentSessionId: string
title: string
}

type HookState = {
sessionStartFired: boolean
sessionId: string
subagentSessions: Map<string, SubagentInfo>
}

export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory, worktree }) => {
const state: HookState = { sessionStartFired: false, sessionId: "" }
const state: HookState = { sessionStartFired: false, sessionId: "", subagentSessions: new Map() }

const fireSessionStart = async (sessionId?: string) => {
if (state.sessionStartFired) return
Expand Down Expand Up @@ -214,6 +220,25 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
output.parts = targetParts
},

"experimental.session.compacting": async (input, output) => {
const results = await callHookRunner($, client, "preCompact", {
sessionId: input.sessionID,
})
if (!results) return
for (const parsed of results) {
if (parsed.additionalContext) {
const ctx = parsed.additionalContext
if (Array.isArray(ctx)) {
for (const c of ctx) {
if (typeof c === "string") output.context.push(c)
}
} else if (typeof ctx === "string") {
output.context.push(ctx)
}
}
}
},

task: async (input, output) => {
const results = await callHookRunner($, client, "preToolUse", {
toolName: "task",
Expand All @@ -237,9 +262,32 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
const props = (event as any).properties || {}

switch (event.type) {
case "session.created":
await fireSessionStart(props.sessionID || props.id || "")
case "session.created": {
const info = props.info || {}
if (info.parentID) {
state.subagentSessions.set(info.id, {
parentSessionId: info.parentID,
title: info.title || "",
})
} else {
await fireSessionStart(info.id || props.sessionID || "")
}
break
}
case "session.status": {
const { sessionID, status } = props
if (status?.type === "idle" && state.subagentSessions.has(sessionID)) {
const sub = state.subagentSessions.get(sessionID)!
state.subagentSessions.delete(sessionID)
await callHookRunner($, client, "subagentStop", {
sessionId: sessionID,
subagentId: sessionID,
subagentName: sub.title,
parentSessionId: sub.parentSessionId,
})
}
break
}
case "session.idle":
await fireSessionEnd(props.sessionID || "")
break
Expand All @@ -249,6 +297,11 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
error: props.error || props.message || "",
})
break
case "session.compacted":
await callHookRunner($, client, "postCompact", {
sessionId: props.sessionID || "",
})
break
}
},
}
Expand Down
46 changes: 46 additions & 0 deletions tests/test_opencode_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ def test_plugin_maps_tool_names(self):
self.assertIn('return "view"', text)
self.assertIn('return "edit"', text)

def test_plugin_has_lifecycle_hooks(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn("experimental.session.compacting", text)
self.assertIn("preCompact", text)
self.assertIn("session.compacted", text)
self.assertIn("postCompact", text)

def test_plugin_has_subagent_tracking(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn("subagentSessions", text)
self.assertIn("subagentStop", text)
self.assertIn("session.status", text)
self.assertIn("subagentId", text)
self.assertIn("subagentName", text)
self.assertIn("parentSessionId", text)

def test_plugin_normalizes_tool_args(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn("normalizeToolArgs", text)
Expand Down Expand Up @@ -353,6 +369,36 @@ def test_skill_normalized_args(self):
)
self.assertEqual(proc.returncode, 0, f"skill postToolUse failed:\n{proc.stderr}")

def test_pre_compact_event(self):
proc = _run_hook(
"preCompact",
{
"sessionId": "bridge-test-compact-001",
},
)
self.assertEqual(proc.returncode, 0, f"preCompact failed:\n{proc.stderr}")

def test_post_compact_event(self):
proc = _run_hook(
"postCompact",
{
"sessionId": "bridge-test-compact-002",
},
)
self.assertEqual(proc.returncode, 0, f"postCompact failed:\n{proc.stderr}")

def test_subagent_stop_event(self):
proc = _run_hook(
"subagentStop",
{
"sessionId": "bridge-test-sub-001",
"subagentId": "bridge-test-sub-001",
"subagentName": "test-subagent",
"parentSessionId": "bridge-test-main-001",
},
)
self.assertEqual(proc.returncode, 0, f"subagentStop failed:\n{proc.stderr}")

def test_skill_without_normalized_skill_field(self):
proc = _run_hook(
"postToolUse",
Expand Down
Loading