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
2 changes: 1 addition & 1 deletion Sources/CodeIsland/ConfigInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodeIsland/RemoteInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
186 changes: 149 additions & 37 deletions Sources/CodeIsland/Resources/codeisland-opencode-remote.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
Expand All @@ -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"]) {
Expand All @@ -90,38 +138,86 @@ 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" });
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) 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" });
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")) {
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",
cwd,
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) {
Expand All @@ -133,65 +229,81 @@ 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,
tool_input: p.part.state?.input || {},
});
}
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;
}
Expand Down
Loading