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
21 changes: 18 additions & 3 deletions opencode-plugin/copilot-tools-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
})
}

const fireSessionEnd = async (sessionId?: string) => {
const fireSessionEnd = async (sessionId?: string, reason?: string) => {
await callHookRunner($, client, "sessionEnd", {
sessionId: sessionId || state.sessionId || "",
reason: reason || "unknown",
})
}

Expand All @@ -127,6 +128,12 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
await fireSessionEnd(input.sessionID)
},

"shell.env": async (input, output) => {
if (!process.env.COPILOT_AGENT_SESSION_ID && input.sessionID) {
output.env.COPILOT_AGENT_SESSION_ID = input.sessionID
}
},

"tool.execute.before": async (input, output) => {
const toolName = mapToolName(input.tool)
const toolArgs = normalizeToolArgs(toolName, output.args ?? {})
Expand All @@ -153,10 +160,17 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
const toolName = mapToolName(input.tool)
const toolArgs = normalizeToolArgs(toolName, (input.args ?? {}) as Record<string, unknown>)

const isError = !!(output as any).isError
const metadata = (output as any).metadata || {}
const exitCode = metadata.exitCode ?? metadata.exit_code ?? (isError ? 1 : 0)

const toolResult: Record<string, unknown> = {
title: output.title,
output: output.output,
resultType: output.isError ? "error" : "success",
stdout: output.output,
resultType: isError ? "error" : "success",
exitCode,
exit_code: exitCode,
}
if (typeof toolArgs.filePath === "string") {
toolResult.filePath = toolArgs.filePath
Expand Down Expand Up @@ -193,6 +207,7 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
sessionId: input.sessionID,
prompt,
additionalContext: [],
cwd: process.cwd(),
})

if (!results) return
Expand Down Expand Up @@ -289,7 +304,7 @@ export const CopilotToolsBridge: Plugin = async ({ project, client, $, directory
break
}
case "session.idle":
await fireSessionEnd(props.sessionID || "")
await fireSessionEnd(props.sessionID || "", "idle")
break
case "session.error":
await callHookRunner($, client, "errorOccurred", {
Expand Down
68 changes: 68 additions & 0 deletions tests/test_opencode_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,27 @@ def test_plugin_has_subagent_tracking(self):
self.assertIn("subagentName", text)
self.assertIn("parentSessionId", text)

def test_plugin_includes_exit_code_in_tool_result(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn("exitCode", text)
self.assertIn("exit_code", text)
self.assertIn("stdout", text)

def test_plugin_includes_reason_in_session_end(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn('reason: reason || "unknown"', text)
self.assertIn(', "idle")', text)

def test_plugin_includes_cwd_in_user_prompt(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn("cwd: process.cwd()", text)
self.assertIn("userPromptSubmitted", text)

def test_plugin_has_shell_env(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn("shell.env", text)
self.assertIn("COPILOT_AGENT_SESSION_ID", text)

def test_plugin_normalizes_tool_args(self):
text = PLUGIN_SRC.read_text(encoding="utf-8")
self.assertIn("normalizeToolArgs", text)
Expand Down Expand Up @@ -301,6 +322,53 @@ def test_session_end_cleanup(self):
)
self.assertEqual(proc.returncode, 0, f"sessionEnd failed:\n{proc.stderr}")

def test_session_end_with_reason(self):
proc = _run_hook(
"sessionEnd",
{
"sessionId": "bridge-test-reason",
"reason": "idle",
},
)
self.assertEqual(proc.returncode, 0, f"sessionEnd with reason failed:\n{proc.stderr}")
stdout = proc.stdout.strip()
if stdout:
parsed = json.loads(stdout)
self.assertIsInstance(parsed, dict)
self.assertIn("message", parsed)

def test_post_tool_use_with_exit_code(self):
proc = _run_hook(
"postToolUse",
{
"toolName": "bash",
"toolArgs": {"command": "echo hello"},
"toolInput": {"command": "echo hello"},
"toolResult": {
"title": "Run command",
"output": "hello",
"stdout": "hello",
"resultType": "success",
"exitCode": 0,
"exit_code": 0,
},
"sessionId": "bridge-test-exit",
},
)
self.assertEqual(proc.returncode, 0, f"postToolUse with exitCode failed:\n{proc.stderr}")

def test_user_prompt_submitted_with_cwd(self):
proc = _run_hook(
"userPromptSubmitted",
{
"sessionId": "bridge-test-cwd",
"prompt": "test prompt with cwd",
"additionalContext": [],
"cwd": "/tmp",
},
)
self.assertEqual(proc.returncode, 0, f"userPromptSubmitted with cwd failed:\n{proc.stderr}")

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