From 1f2ee13b9cce0a93df77643cfd7511220305592a Mon Sep 17 00:00:00 2001 From: x22x22 Date: Fri, 10 Jul 2026 14:17:09 +0800 Subject: [PATCH 1/2] fix: support new OpenCode plugin events --- Sources/CodeIsland/ConfigInstaller.swift | 2 +- Sources/CodeIsland/RemoteInstaller.swift | 2 +- .../Resources/codeisland-opencode-remote.js | 182 +++++++++++++---- .../Resources/codeisland-opencode.js | 188 ++++++++++++++---- 4 files changed, 294 insertions(+), 80 deletions(-) diff --git a/Sources/CodeIsland/ConfigInstaller.swift b/Sources/CodeIsland/ConfigInstaller.swift index 1470b67e..ced6cde9 100644 --- a/Sources/CodeIsland/ConfigInstaller.swift +++ b/Sources/CodeIsland/ConfigInstaller.swift @@ -3045,7 +3045,7 @@ struct ConfigInstaller { } /// Current OpenCode plugin version — bump when codeisland-opencode.js changes - private static let opencodePluginVersion = "v4" + private static let opencodePluginVersion = "v6" private static func isOpencodePluginInstalled(fm: FileManager) -> Bool { guard fm.fileExists(atPath: opencodePluginPath) else { return false } diff --git a/Sources/CodeIsland/RemoteInstaller.swift b/Sources/CodeIsland/RemoteInstaller.swift index 0bbfb0f2..adef7ae4 100644 --- a/Sources/CodeIsland/RemoteInstaller.swift +++ b/Sources/CodeIsland/RemoteInstaller.swift @@ -15,7 +15,7 @@ private struct RemoteCommandResult: Sendable { enum RemoteInstaller { private static let remoteHookVersion = "0.1.2" - private static let remoteOpencodePluginVersion = "v2" + private static let remoteOpencodePluginVersion = "v3" static func installAll(host: RemoteHost, remoteSocketPath: String) async -> RemoteInstallResult { guard let source = remoteHookSource() else { diff --git a/Sources/CodeIsland/Resources/codeisland-opencode-remote.js b/Sources/CodeIsland/Resources/codeisland-opencode-remote.js index 7d26db48..8e540cf8 100644 --- a/Sources/CodeIsland/Resources/codeisland-opencode-remote.js +++ b/Sources/CodeIsland/Resources/codeisland-opencode-remote.js @@ -1,7 +1,7 @@ // codeisland-opencode-remote-plugin — auto-generated by CodeIsland -// version: v2 +// version: v3 // Remote OpenCode event forwarding via ~/.codeisland/codeisland-remote-hook.py -import { execFile } from "child_process"; +import { execFile, execSync } from "child_process"; const HOOK_PATH = `${process.env.HOME}/.codeisland/codeisland-remote-hook.py`; const SOCKET_PATH = process.env.CODEISLAND_SOCKET_PATH || "/tmp/codeisland.sock"; @@ -41,12 +41,12 @@ export default { const msgRoles = new Map(); const sessionCwd = new Map(); const sessions = new Map(); + const toolNamesByCall = new Map(); const pendingRequestSessions = new Set(); const heyApi = client?._client; let detectedTty = null; try { - const { execSync } = await import("child_process"); let walkPid = process.pid; for (let i = 0; i < 8; i++) { const info = execSync(`ps -o tty=,ppid= -p ${walkPid}`, { timeout: 1000 }).toString().trim(); @@ -64,6 +64,54 @@ export default { return sessions.get(sid); } + function sessionKey(sessionId) { + return String(sessionId || ""); + } + + function islandSessionId(sessionId) { + return `opencode-${sessionKey(sessionId)}`; + } + + function prettyToolName(name) { + const raw = String(name || ""); + return raw.charAt(0).toUpperCase() + raw.slice(1); + } + + function setCwd(sessionId, cwd) { + if (sessionId && cwd) sessionCwd.set(sessionKey(sessionId), cwd); + } + + function cwdFor(sessionId) { + return sessionCwd.get(sessionKey(sessionId)) || process.env.PWD || ""; + } + + function rememberTool(sessionId, callId, name) { + if (!sessionId || !callId || !name) return; + toolNamesByCall.set(`${sessionKey(sessionId)}:${callId}`, prettyToolName(name)); + if (toolNamesByCall.size > 200) toolNamesByCall.delete(toolNamesByCall.keys().next().value); + } + + function rememberedTool(sessionId, callId) { + return toolNamesByCall.get(`${sessionKey(sessionId)}:${callId}`) || "Tool"; + } + + function mapQuestions(questions) { + return (questions || []).map(q => ({ + question: q.question || "", + header: q.header || "", + options: (q.options || []).map(o => ({ label: o.label, description: o.description })), + multiSelect: q.multiple || false, + })); + } + + function permissionInput(action, resources, metadata) { + const items = resources || []; + const toolInput = { patterns: items, metadata }; + if (action === "bash" && items.length > 0) toolInput.command = items.join(" && "); + if ((action === "edit" || action === "write") && items.length > 0) toolInput.file_path = items[0]; + return toolInput; + } + function collectEnv() { const env = {}; for (const key of ["TERM_PROGRAM", "ITERM_SESSION_ID", "TERM_SESSION_ID", "TMUX", "TMUX_PANE", "KITTY_WINDOW_ID", "__CFBundleIdentifier"]) { @@ -90,30 +138,78 @@ export default { const t = ev.type; const p = ev.properties || {}; if (t === "session.created" && p.info) { - const cwd = p.info.directory || ""; - sessionCwd.set(p.info.id, cwd); - return base(`opencode-${p.info.id}`, { hook_event_name: "SessionStart", cwd }); + const cwd = p.info.directory || p.info.location?.directory || ""; + setCwd(p.info.id, cwd); + return base(islandSessionId(p.info.id), { hook_event_name: "SessionStart", cwd }); } if (t === "session.deleted" && p.info) { sessions.delete(p.info.id); sessionCwd.delete(p.info.id); - return base(`opencode-${p.info.id}`, { hook_event_name: "SessionEnd" }); + return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (t === "session.updated" && p.info) { - if (p.info.directory) sessionCwd.set(p.info.id, p.info.directory); + if (p.info.directory || p.info.location?.directory) setCwd(p.info.id, p.info.directory || p.info.location?.directory); if (p.info.time?.archived) { sessions.delete(p.info.id); sessionCwd.delete(p.info.id); - return base(`opencode-${p.info.id}`, { hook_event_name: "SessionEnd" }); + return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (p.info.title && !p.info.title.startsWith("New session")) { getSession(p.info.id).pendingTitle = p.info.title; } return null; } + if (t === "session.next.moved" && p.sessionID) { + const cwd = p.location?.directory || ""; + setCwd(p.sessionID, cwd); + return base(islandSessionId(p.sessionID), { hook_event_name: "SessionStart", cwd }); + } + if (t === "session.next.prompted" || t === "session.next.prompt.admitted") { + if (!p.sessionID) return null; + const cwd = cwdFor(p.sessionID); + const prompt = p.prompt?.text || p.text || ""; + if (!prompt) return base(islandSessionId(p.sessionID), { hook_event_name: "UserPromptSubmit", cwd }); + getSession(p.sessionID).lastUserText = prompt; + return base(islandSessionId(p.sessionID), { hook_event_name: "UserPromptSubmit", cwd, prompt }); + } + if (t === "session.next.text.delta" && p.sessionID) { + const s = getSession(p.sessionID); + const key = p.textID || "default"; + s.textParts = s.textParts || new Map(); + s.textParts.set(key, (s.textParts.get(key) || "") + (p.delta || "")); + s.lastAssistantText = Array.from(s.textParts.values()).join("\n"); + return null; + } + if (t === "session.next.text.ended" && p.sessionID) { + const s = getSession(p.sessionID); + if (p.text) s.lastAssistantText = p.text; + return null; + } + if (t === "session.next.compaction.started" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PreCompact", cwd: cwdFor(p.sessionID) }); + } + if (t === "session.next.tool.called" && p.sessionID) { + const toolName = prettyToolName(p.tool || p.name); + rememberTool(p.sessionID, p.callID, toolName); + return base(islandSessionId(p.sessionID), { + hook_event_name: "PreToolUse", + cwd: cwdFor(p.sessionID), + tool_name: toolName, + tool_input: p.input || {}, + }); + } + if (t === "session.next.tool.success" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID), tool_name: rememberedTool(p.sessionID, p.callID) }); + } + if (t === "session.next.tool.failed" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUseFailure", cwd: cwdFor(p.sessionID), tool_name: rememberedTool(p.sessionID, p.callID) }); + } if (t === "session.status" && p.sessionID) { const s = getSession(p.sessionID); - const cwd = sessionCwd.get(p.sessionID); + const cwd = cwdFor(p.sessionID); + if (p.status?.type === "busy" || p.status?.type === "retry") { + return base(islandSessionId(p.sessionID), { hook_event_name: "UserPromptSubmit", cwd }); + } if (p.status?.type === "idle") { const extra = { hook_event_name: "Stop", @@ -121,7 +217,7 @@ export default { last_assistant_message: s.lastAssistantText || undefined, }; if (s.pendingTitle) { extra.codex_title = s.pendingTitle; s.pendingTitle = null; } - return base(`opencode-${p.sessionID}`, extra); + return base(islandSessionId(p.sessionID), extra); } } if (t === "message.updated" && p.info?.id && p.info?.sessionID) { @@ -133,21 +229,21 @@ export default { const meta = msgRoles.get(p.part.messageID); if (!meta) return null; const s = getSession(meta.sessionID); - const cwd = sessionCwd.get(meta.sessionID); + const cwd = cwdFor(meta.sessionID); const text = p.part.text || ""; if (meta.role === "user" && text) { s.lastUserText = text; - return base(`opencode-${meta.sessionID}`, { hook_event_name: "UserPromptSubmit", cwd, prompt: text }); + return base(islandSessionId(meta.sessionID), { hook_event_name: "UserPromptSubmit", cwd, prompt: text }); } if (meta.role === "assistant" && text) s.lastAssistantText = text; return null; } if (t === "message.part.updated" && p.part?.type === "tool" && p.part?.sessionID) { - const cwd = sessionCwd.get(p.part.sessionID); + const cwd = cwdFor(p.part.sessionID); const st = p.part.state?.status; - const toolName = (p.part.tool || "").charAt(0).toUpperCase() + (p.part.tool || "").slice(1); + const toolName = prettyToolName(p.part.tool || p.part.name); if (st === "running" || st === "pending") { - return base(`opencode-${p.part.sessionID}`, { + return base(islandSessionId(p.part.sessionID), { hook_event_name: "PreToolUse", cwd, tool_name: toolName, @@ -155,43 +251,59 @@ export default { }); } if (st === "completed" || st === "error") { - return base(`opencode-${p.part.sessionID}`, { hook_event_name: "PostToolUse", cwd, tool_name: toolName }); + return base(islandSessionId(p.part.sessionID), { hook_event_name: "PostToolUse", cwd, tool_name: toolName }); } } if (t === "permission.asked" && p.id && p.sessionID) { - const toolName = (p.permission || "").charAt(0).toUpperCase() + (p.permission || "").slice(1); - const patterns = p.patterns || []; - const toolInput = { patterns, metadata: p.metadata }; - if (p.permission === "bash" && patterns.length > 0) toolInput.command = patterns.join(" && "); - if ((p.permission === "edit" || p.permission === "write") && patterns.length > 0) toolInput.file_path = patterns[0]; - return base(`opencode-${p.sessionID}`, { + const toolName = prettyToolName(p.permission); + const toolInput = permissionInput(p.permission, p.patterns, p.metadata); + return base(islandSessionId(p.sessionID), { hook_event_name: "PermissionRequest", - cwd: sessionCwd.get(p.sessionID), + cwd: cwdFor(p.sessionID), tool_name: toolName, tool_input: toolInput, _opencode_request_id: p.id, }); } if (t === "permission.replied" && p.sessionID) { - return base(`opencode-${p.sessionID}`, { hook_event_name: "PostToolUse", cwd: sessionCwd.get(p.sessionID) }); + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID) }); + } + if (t === "permission.v2.asked" && p.id && p.sessionID) { + const toolInput = permissionInput(p.action, p.resources, p.metadata); + return base(islandSessionId(p.sessionID), { + hook_event_name: "PermissionRequest", + cwd: cwdFor(p.sessionID), + tool_name: prettyToolName(p.action), + tool_input: toolInput, + _opencode_request_id: p.id, + }); + } + if (t === "permission.v2.replied" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID) }); } if (t === "question.asked" && p.id && p.sessionID) { - const questions = (p.questions || []).map(q => ({ - question: q.question || "", - header: q.header || "", - options: (q.options || []).map(o => ({ label: o.label, description: o.description })), - multiSelect: q.multiple || false, - })); - return base(`opencode-${p.sessionID}`, { + return base(islandSessionId(p.sessionID), { hook_event_name: "PermissionRequest", - cwd: sessionCwd.get(p.sessionID), + cwd: cwdFor(p.sessionID), tool_name: "AskUserQuestion", - tool_input: { questions }, + tool_input: { questions: mapQuestions(p.questions) }, _opencode_request_id: p.id, }); } if ((t === "question.replied" || t === "question.rejected") && p.sessionID) { - return base(`opencode-${p.sessionID}`, { hook_event_name: "PostToolUse", cwd: sessionCwd.get(p.sessionID) }); + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID) }); + } + if (t === "question.v2.asked" && p.id && p.sessionID) { + return base(islandSessionId(p.sessionID), { + hook_event_name: "PermissionRequest", + cwd: cwdFor(p.sessionID), + tool_name: "AskUserQuestion", + tool_input: { questions: mapQuestions(p.questions) }, + _opencode_request_id: p.id, + }); + } + if ((t === "question.v2.replied" || t === "question.v2.rejected") && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID) }); } return null; } diff --git a/Sources/CodeIsland/Resources/codeisland-opencode.js b/Sources/CodeIsland/Resources/codeisland-opencode.js index b0248602..2674e6e0 100644 --- a/Sources/CodeIsland/Resources/codeisland-opencode.js +++ b/Sources/CodeIsland/Resources/codeisland-opencode.js @@ -1,8 +1,12 @@ // codeisland-opencode-plugin — auto-generated by CodeIsland -// version: v4 +// version: v6 // OpenCode event forwarding + terminal env injection + permission/question handling import { connect } from "net"; import { getuid } from "process"; +import { execFile, execSync } from "child_process"; +import { writeFileSync } from "fs"; +import { homedir } from "os"; +import { join } from "path"; const SOCKET = `/tmp/codeisland-${getuid()}.sock`; @@ -24,12 +28,12 @@ function sendToSocket(json) { // uses POSIX sockets. Node.js net module's half-close (sock.end()) causes NWConnection // to close immediately on macOS, losing the response. The bridge's shutdown(SHUT_WR) // works correctly with NWConnection. -const BRIDGE_PATH = require("path").join(require("os").homedir(), ".codeisland", "codeisland-bridge"); +const BRIDGE_PATH = join(homedir(), ".codeisland", "codeisland-bridge"); function sendAndWaitResponse(json, timeoutMs = 300000) { return new Promise((resolve) => { try { - const child = require("child_process").execFile(BRIDGE_PATH, [], { + const child = execFile(BRIDGE_PATH, [], { timeout: timeoutMs, maxBuffer: 1024 * 1024, }, (error, stdout) => { if (error) { resolve(null); return; } @@ -49,6 +53,7 @@ export default { const serverPort = serverUrl ? parseInt(serverUrl.port) || 4096 : 4096; const msgRoles = new Map(); const sessions = new Map(); + const toolNamesByCall = new Map(); // OpenCode client SDK (_client is @hey-api/client-fetch with non-enumerable methods) const heyApi = client?._client; @@ -66,7 +71,6 @@ export default { let detectedTty = null; try { - const { execSync } = require("child_process"); let walkPid = process.pid; for (let i = 0; i < 8; i++) { const info = execSync(`ps -o tty=,ppid= -p ${walkPid}`, { timeout: 1000 }).toString().trim(); @@ -103,11 +107,59 @@ export default { : `${project} \u00b7 ${prefix}`; const osc = `\x1b]2;${title}\x07`; if (detectedTty) { - try { require("fs").writeFileSync(detectedTty, osc); } catch {} + try { writeFileSync(detectedTty, osc); } catch {} } } // Base fields for every event + function sessionKey(sessionId) { + return String(sessionId || ""); + } + + function islandSessionId(sessionId) { + return `opencode-${sessionKey(sessionId)}`; + } + + function prettyToolName(name) { + const raw = String(name || ""); + return raw.charAt(0).toUpperCase() + raw.slice(1); + } + + function setCwd(sessionId, cwd) { + if (sessionId && cwd) sessionCwd.set(sessionKey(sessionId), cwd); + } + + function cwdFor(sessionId) { + return sessionCwd.get(sessionKey(sessionId)) || process.env.PWD || ""; + } + + function rememberTool(sessionId, callId, name) { + if (!sessionId || !callId || !name) return; + toolNamesByCall.set(`${sessionKey(sessionId)}:${callId}`, prettyToolName(name)); + if (toolNamesByCall.size > 200) toolNamesByCall.delete(toolNamesByCall.keys().next().value); + } + + function rememberedTool(sessionId, callId) { + return toolNamesByCall.get(`${sessionKey(sessionId)}:${callId}`) || "Tool"; + } + + function mapQuestions(questions) { + return (questions || []).map(q => ({ + question: q.question || "", + header: q.header || "", + options: (q.options || []).map(o => ({ label: o.label, description: o.description })), + multiSelect: q.multiple || false, + })); + } + + function permissionInput(action, resources, metadata) { + const items = resources || []; + const toolInput = { patterns: items, metadata }; + if (action === "bash" && items.length > 0) toolInput.command = items.join(" && "); + if ((action === "edit" || action === "write") && items.length > 0) toolInput.file_path = items[0]; + return toolInput; + } + function base(sessionId, extra) { return { session_id: sessionId, _source: "opencode", _ppid: pid, _env: collectEnv(), _tty: detectedTty, _server_port: serverPort, ...extra }; @@ -124,31 +176,77 @@ export default { const p = ev.properties || {}; if (t === "session.created" && p.info) { - const cwd = p.info.directory || ""; - sessionCwd.set(p.info.id, cwd); - return base(`opencode-${p.info.id}`, { hook_event_name: "SessionStart", cwd }); + const cwd = p.info.directory || p.info.location?.directory || ""; + setCwd(p.info.id, cwd); + return base(islandSessionId(p.info.id), { hook_event_name: "SessionStart", cwd }); } if (t === "session.deleted" && p.info) { sessions.delete(p.info.id); sessionCwd.delete(p.info.id); - return base(`opencode-${p.info.id}`, { hook_event_name: "SessionEnd" }); + return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (t === "session.updated" && p.info) { - if (p.info.directory) sessionCwd.set(p.info.id, p.info.directory); + if (p.info.directory || p.info.location?.directory) setCwd(p.info.id, p.info.directory || p.info.location?.directory); if (p.info.time?.archived) { sessions.delete(p.info.id); sessionCwd.delete(p.info.id); - return base(`opencode-${p.info.id}`, { hook_event_name: "SessionEnd" }); + return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (p.info.title && !p.info.title.startsWith("New session")) { const s = getSession(p.info.id); s.pendingTitle = p.info.title; - setTabTitle(`opencode-${p.info.id}`, sessionCwd.get(p.info.id), null, p.info.title); + setTabTitle(islandSessionId(p.info.id), cwdFor(p.info.id), null, p.info.title); } return null; } + if (t === "session.next.moved" && p.sessionID) { + const cwd = p.location?.directory || ""; + setCwd(p.sessionID, cwd); + return base(islandSessionId(p.sessionID), { hook_event_name: "SessionStart", cwd }); + } + if (t === "session.next.prompted" || t === "session.next.prompt.admitted") { + if (!p.sessionID) return null; + const cwd = cwdFor(p.sessionID); + const prompt = p.prompt?.text || p.text || ""; + if (!prompt) return base(islandSessionId(p.sessionID), { hook_event_name: "UserPromptSubmit", cwd }); + getSession(p.sessionID).lastUserText = prompt; + return base(islandSessionId(p.sessionID), { hook_event_name: "UserPromptSubmit", cwd, prompt }); + } + if (t === "session.next.text.delta" && p.sessionID) { + const s = getSession(p.sessionID); + const key = p.textID || "default"; + s.textParts = s.textParts || new Map(); + s.textParts.set(key, (s.textParts.get(key) || "") + (p.delta || "")); + s.lastAssistantText = Array.from(s.textParts.values()).join("\n"); + return null; + } + if (t === "session.next.text.ended" && p.sessionID) { + const s = getSession(p.sessionID); + if (p.text) s.lastAssistantText = p.text; + return null; + } + if (t === "session.next.compaction.started" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PreCompact", cwd: cwdFor(p.sessionID) }); + } + if (t === "session.next.tool.called" && p.sessionID) { + const toolName = prettyToolName(p.tool || p.name); + rememberTool(p.sessionID, p.callID, toolName); + return base(islandSessionId(p.sessionID), { hook_event_name: "PreToolUse", cwd: cwdFor(p.sessionID), tool_name: toolName, + tool_input: p.input || {} }); + } + if (t === "session.next.tool.success" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID), + tool_name: rememberedTool(p.sessionID, p.callID) }); + } + if (t === "session.next.tool.failed" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUseFailure", cwd: cwdFor(p.sessionID), + tool_name: rememberedTool(p.sessionID, p.callID) }); + } if (t === "session.status" && p.sessionID) { - const sid = `opencode-${p.sessionID}`; + const sid = islandSessionId(p.sessionID); const s = getSession(p.sessionID); - const cwd = sessionCwd.get(p.sessionID); + const cwd = cwdFor(p.sessionID); + if (p.status?.type === "busy" || p.status?.type === "retry") { + return base(sid, { hook_event_name: "UserPromptSubmit", cwd }); + } if (p.status?.type === "idle") { const extra = { hook_event_name: "Stop", cwd, last_assistant_message: s.lastAssistantText || undefined }; @@ -165,21 +263,21 @@ export default { const meta = msgRoles.get(p.part.messageID); if (!meta) return null; const s = getSession(meta.sessionID); - const cwd = sessionCwd.get(meta.sessionID); + const cwd = cwdFor(meta.sessionID); const text = p.part.text || ""; if (meta.role === "user" && text) { s.lastUserText = text; - return base(`opencode-${meta.sessionID}`, { + return base(islandSessionId(meta.sessionID), { hook_event_name: "UserPromptSubmit", cwd, prompt: text }); } if (meta.role === "assistant" && text) { s.lastAssistantText = text; } return null; } if (t === "message.part.updated" && p.part?.type === "tool" && p.part?.sessionID) { - const sid = `opencode-${p.part.sessionID}`; + const sid = islandSessionId(p.part.sessionID); const st = p.part.state?.status; - const cwd = sessionCwd.get(p.part.sessionID); - const toolName = (p.part.tool || "").charAt(0).toUpperCase() + (p.part.tool || "").slice(1); + const cwd = cwdFor(p.part.sessionID); + const toolName = prettyToolName(p.part.tool || p.part.name); if (st === "running" || st === "pending") { return base(sid, { hook_event_name: "PreToolUse", cwd, tool_name: toolName, tool_input: p.part.state?.input || {} }); @@ -189,37 +287,41 @@ export default { } } if (t === "permission.asked" && p.id && p.sessionID) { - const toolName = (p.permission || "").charAt(0).toUpperCase() + (p.permission || "").slice(1); - const patterns = p.patterns || []; - const toolInput = { patterns, metadata: p.metadata }; - if (p.permission === "bash" && patterns.length > 0) { - toolInput.command = patterns.join(" && "); - } - if ((p.permission === "edit" || p.permission === "write") && patterns.length > 0) { - toolInput.file_path = patterns[0]; - } - return base(`opencode-${p.sessionID}`, { hook_event_name: "PermissionRequest", - cwd: sessionCwd.get(p.sessionID), tool_name: toolName, + const toolName = prettyToolName(p.permission); + const toolInput = permissionInput(p.permission, p.patterns, p.metadata); + return base(islandSessionId(p.sessionID), { hook_event_name: "PermissionRequest", + cwd: cwdFor(p.sessionID), tool_name: toolName, tool_input: toolInput, _opencode_request_id: p.id }); } if (t === "permission.replied" && p.sessionID) { - return base(`opencode-${p.sessionID}`, { hook_event_name: "PostToolUse", - cwd: sessionCwd.get(p.sessionID) }); + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", + cwd: cwdFor(p.sessionID) }); + } + if (t === "permission.v2.asked" && p.id && p.sessionID) { + const toolInput = permissionInput(p.action, p.resources, p.metadata); + return base(islandSessionId(p.sessionID), { hook_event_name: "PermissionRequest", + cwd: cwdFor(p.sessionID), tool_name: prettyToolName(p.action), + tool_input: toolInput, _opencode_request_id: p.id }); + } + if (t === "permission.v2.replied" && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID) }); } if (t === "question.asked" && p.id && p.sessionID) { - const questions = (p.questions || []).map(q => ({ - question: q.question || "", - header: q.header || "", - options: (q.options || []).map(o => ({ label: o.label, description: o.description })), - multiSelect: q.multiple || false, - })); - return base(`opencode-${p.sessionID}`, { hook_event_name: "PermissionRequest", - cwd: sessionCwd.get(p.sessionID), tool_name: "AskUserQuestion", - tool_input: { questions }, _opencode_request_id: p.id }); + return base(islandSessionId(p.sessionID), { hook_event_name: "PermissionRequest", + cwd: cwdFor(p.sessionID), tool_name: "AskUserQuestion", + tool_input: { questions: mapQuestions(p.questions) }, _opencode_request_id: p.id }); } if ((t === "question.replied" || t === "question.rejected") && p.sessionID) { - return base(`opencode-${p.sessionID}`, { hook_event_name: "PostToolUse", - cwd: sessionCwd.get(p.sessionID) }); + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", + cwd: cwdFor(p.sessionID) }); + } + if (t === "question.v2.asked" && p.id && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PermissionRequest", + cwd: cwdFor(p.sessionID), tool_name: "AskUserQuestion", + tool_input: { questions: mapQuestions(p.questions) }, _opencode_request_id: p.id }); + } + if ((t === "question.v2.replied" || t === "question.v2.rejected") && p.sessionID) { + return base(islandSessionId(p.sessionID), { hook_event_name: "PostToolUse", cwd: cwdFor(p.sessionID) }); } return null; } From 3a6549b2c92e83a405e11e8c7d6eebc1c03f1adb Mon Sep 17 00:00:00 2001 From: x22x22 Date: Fri, 10 Jul 2026 14:23:48 +0800 Subject: [PATCH 2/2] fix: clear OpenCode cwd entries consistently --- Sources/CodeIsland/Resources/codeisland-opencode-remote.js | 4 ++-- Sources/CodeIsland/Resources/codeisland-opencode.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CodeIsland/Resources/codeisland-opencode-remote.js b/Sources/CodeIsland/Resources/codeisland-opencode-remote.js index 8e540cf8..959dffa3 100644 --- a/Sources/CodeIsland/Resources/codeisland-opencode-remote.js +++ b/Sources/CodeIsland/Resources/codeisland-opencode-remote.js @@ -144,14 +144,14 @@ export default { } if (t === "session.deleted" && p.info) { sessions.delete(p.info.id); - sessionCwd.delete(p.info.id); + sessionCwd.delete(sessionKey(p.info.id)); return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (t === "session.updated" && p.info) { if (p.info.directory || p.info.location?.directory) setCwd(p.info.id, p.info.directory || p.info.location?.directory); if (p.info.time?.archived) { sessions.delete(p.info.id); - sessionCwd.delete(p.info.id); + sessionCwd.delete(sessionKey(p.info.id)); return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (p.info.title && !p.info.title.startsWith("New session")) { diff --git a/Sources/CodeIsland/Resources/codeisland-opencode.js b/Sources/CodeIsland/Resources/codeisland-opencode.js index 2674e6e0..cbd706e2 100644 --- a/Sources/CodeIsland/Resources/codeisland-opencode.js +++ b/Sources/CodeIsland/Resources/codeisland-opencode.js @@ -181,13 +181,13 @@ export default { return base(islandSessionId(p.info.id), { hook_event_name: "SessionStart", cwd }); } if (t === "session.deleted" && p.info) { - sessions.delete(p.info.id); sessionCwd.delete(p.info.id); + sessions.delete(p.info.id); sessionCwd.delete(sessionKey(p.info.id)); return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (t === "session.updated" && p.info) { if (p.info.directory || p.info.location?.directory) setCwd(p.info.id, p.info.directory || p.info.location?.directory); if (p.info.time?.archived) { - sessions.delete(p.info.id); sessionCwd.delete(p.info.id); + sessions.delete(p.info.id); sessionCwd.delete(sessionKey(p.info.id)); return base(islandSessionId(p.info.id), { hook_event_name: "SessionEnd" }); } if (p.info.title && !p.info.title.startsWith("New session")) {