Move OpenCode UI into Electron shell

This commit is contained in:
2026-05-11 23:31:09 +00:00
parent 6f32a206b4
commit 08ed394737
40 changed files with 901 additions and 881 deletions
+8 -3
View File
@@ -10,14 +10,19 @@
"package": "pnpm run build && electron-builder --config electron-builder.yml" "package": "pnpm run build && electron-builder --config electron-builder.yml"
}, },
"dependencies": { "dependencies": {
"@git.zone/ide-protocol": "workspace:*",
"@git.zone/ide-opencode-bridge": "workspace:*", "@git.zone/ide-opencode-bridge": "workspace:*",
"@git.zone/ide-protocol": "workspace:*",
"@git.zone/ide-server-installer": "workspace:*", "@git.zone/ide-server-installer": "workspace:*",
"@git.zone/ide-ssh": "workspace:*", "@git.zone/ide-ssh": "workspace:*",
"electron": "^42.0.1" "electron": "^42.0.1",
"opencode-ai": "1.14.48"
}, },
"devDependencies": { "devDependencies": {
"electron-builder": "^26.8.1" "electron-builder": "^26.8.1"
}, },
"files": ["dist_ts/**/*", "preload.cjs", "electron-builder.yml"] "files": [
"dist_ts/**/*",
"preload.cjs",
"electron-builder.yml"
]
} }
+16
View File
@@ -6,6 +6,22 @@ contextBridge.exposeInMainWorld('gitZoneIde', {
connect: (input) => ipcRenderer.invoke('gitzone:connect', input), connect: (input) => ipcRenderer.invoke('gitzone:connect', input),
addProject: (input) => ipcRenderer.invoke('gitzone:add-project', input), addProject: (input) => ipcRenderer.invoke('gitzone:add-project', input),
openProject: (input) => ipcRenderer.invoke('gitzone:open-project', input), openProject: (input) => ipcRenderer.invoke('gitzone:open-project', input),
openCode: {
health: (input) => ipcRenderer.invoke('gitzone:opencode-health', input),
sessions: (input) => ipcRenderer.invoke('gitzone:opencode-sessions', input),
createSession: (input) => ipcRenderer.invoke('gitzone:opencode-create-session', input),
messages: (input) => ipcRenderer.invoke('gitzone:opencode-messages', input),
prompt: (input) => ipcRenderer.invoke('gitzone:opencode-prompt', input),
abort: (input) => ipcRenderer.invoke('gitzone:opencode-abort', input),
respondToPermission: (input) => ipcRenderer.invoke('gitzone:opencode-respond-permission', input),
providers: (input) => ipcRenderer.invoke('gitzone:opencode-providers', input),
agents: (input) => ipcRenderer.invoke('gitzone:opencode-agents', input),
onEvent: (callback) => {
const listener = (_event, payload) => callback(payload);
ipcRenderer.on('gitzone:opencode-event', listener);
return () => ipcRenderer.removeListener('gitzone:opencode-event', listener);
},
},
onConnectProgress: (callback) => { onConnectProgress: (callback) => {
const listener = (_event, message) => callback(message); const listener = (_event, message) => callback(message);
ipcRenderer.on('gitzone:connect-progress', listener); ipcRenderer.on('gitzone:connect-progress', listener);
+712 -25
View File
@@ -213,6 +213,76 @@ class GitZoneIdeElectronShell {
progress(`Project ${project.title} is ready.`); progress(`Project ${project.title} is ready.`);
return instance; return instance;
}); });
plugins.electron.ipcMain.handle('gitzone:opencode-health', async (_event, input: IOpenCodeRuntimeInput) => {
return this.createLocalOpenCodeClient(input.instanceId).health();
});
plugins.electron.ipcMain.handle('gitzone:opencode-sessions', async (_event, input: IOpenCodeRuntimeInput) => {
return this.createLocalOpenCodeClient(input.instanceId).sessions();
});
plugins.electron.ipcMain.handle('gitzone:opencode-create-session', async (_event, input: IOpenCodeCreateSessionInput) => {
return this.createLocalOpenCodeClient(input.instanceId).createSession({
title: trimOptional(input.title),
});
});
plugins.electron.ipcMain.handle('gitzone:opencode-messages', async (_event, input: IOpenCodeSessionInput) => {
return this.createLocalOpenCodeClient(input.instanceId).messages(
requireTrimmed(input.sessionId, 'OpenCode session id'),
normalizeOptionalLimit(input.limit, 200),
);
});
plugins.electron.ipcMain.handle('gitzone:opencode-prompt', async (_event, input: IOpenCodePromptInput) => {
const text = requireBoundedText(input.text, 'Prompt', 200000);
await this.createLocalOpenCodeClient(input.instanceId).promptAsync(
requireTrimmed(input.sessionId, 'OpenCode session id'),
{
agent: trimOptional(input.agent),
parts: [{ type: 'text', text }],
},
);
return { ok: true };
});
plugins.electron.ipcMain.handle('gitzone:opencode-abort', async (_event, input: IOpenCodeSessionInput) => {
return this.createLocalOpenCodeClient(input.instanceId).abort(requireTrimmed(input.sessionId, 'OpenCode session id'));
});
plugins.electron.ipcMain.handle('gitzone:opencode-respond-permission', async (_event, input: IOpenCodePermissionInput) => {
return this.createLocalOpenCodeClient(input.instanceId).respondToPermission(
requireTrimmed(input.sessionId, 'OpenCode session id'),
requireTrimmed(input.permissionId, 'OpenCode permission id'),
{
response: requireTrimmed(input.response, 'OpenCode permission response'),
remember: input.remember,
},
);
});
plugins.electron.ipcMain.handle('gitzone:opencode-providers', async (_event, input: IOpenCodeRuntimeInput) => {
return this.createLocalOpenCodeClient(input.instanceId).providers();
});
plugins.electron.ipcMain.handle('gitzone:opencode-agents', async (_event, input: IOpenCodeRuntimeInput) => {
return this.createLocalOpenCodeClient(input.instanceId).agents();
});
}
private requireLocalOpenCodeRuntime(instanceId: string | undefined) {
const id = requireTrimmed(instanceId, 'OpenCode runtime id');
const runtime = this.localOpenCodeRuntimes.get(id);
if (!runtime) {
throw new Error(`OpenCode runtime not found: ${id}`);
}
return runtime;
}
private createLocalOpenCodeClient(instanceId: string | undefined) {
const runtime = this.requireLocalOpenCodeRuntime(instanceId);
return createLocalOpenCodeClient(runtime);
} }
private async ensureToolBridge() { private async ensureToolBridge() {
@@ -292,6 +362,7 @@ class GitZoneIdeElectronShell {
}); });
await waitForOpenCodeHealth(baseUrl, username, password, 15000); await waitForOpenCodeHealth(baseUrl, username, password, 15000);
startOpenCodeEventStream(runtime);
progress(`Local OpenCode server ready for ${project.title}; remote tools are bridged over SSH.`); progress(`Local OpenCode server ready for ${project.title}; remote tools are bridged over SSH.`);
return { return {
status: 'ready', status: 'ready',
@@ -385,6 +456,30 @@ interface IOpenProjectInput {
projectPath?: string; projectPath?: string;
} }
interface IOpenCodeRuntimeInput {
instanceId?: string;
}
interface IOpenCodeCreateSessionInput extends IOpenCodeRuntimeInput {
title?: string;
}
interface IOpenCodeSessionInput extends IOpenCodeRuntimeInput {
sessionId?: string;
limit?: number;
}
interface IOpenCodePromptInput extends IOpenCodeSessionInput {
text?: string;
agent?: string;
}
interface IOpenCodePermissionInput extends IOpenCodeSessionInput {
permissionId?: string;
response?: string;
remember?: boolean;
}
interface IRemoteProject { interface IRemoteProject {
id: string; id: string;
path: string; path: string;
@@ -425,6 +520,7 @@ interface ILocalOpenCodeRuntime {
process: childProcess.ChildProcess; process: childProcess.ChildProcess;
bridge: LocalOpenCodeToolBridge; bridge: LocalOpenCodeToolBridge;
bridgeToken: string; bridgeToken: string;
eventAbortController?: AbortController;
configDir: string; configDir: string;
proxyWorkspacePath: string; proxyWorkspacePath: string;
baseUrl: string; baseUrl: string;
@@ -649,12 +745,13 @@ const writeOpenCodeBridgeConfig = async (configDir: string) => {
}; };
const resolveOpenCodeExecutable = async () => { const resolveOpenCodeExecutable = async () => {
const executableName = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
const candidates = [ const candidates = [
process.env.GITZONE_IDE_OPENCODE_BINARY, process.env.GITZONE_IDE_OPENCODE_BINARY,
process.env.OPENCODE_BINARY, path.join(electronPackageRoot, 'node_modules', '.bin', executableName),
path.join(os.homedir(), '.opencode', 'bin', 'opencode'), path.join(workspaceRoot, 'node_modules', '.bin', executableName),
'/usr/local/bin/opencode', path.join(electronPackageRoot, 'node_modules', 'opencode-ai', 'bin', 'opencode'),
'/usr/bin/opencode', path.join(workspaceRoot, 'node_modules', 'opencode-ai', 'bin', 'opencode'),
].filter(Boolean) as string[]; ].filter(Boolean) as string[];
for (const candidate of candidates) { for (const candidate of candidates) {
try { try {
@@ -662,7 +759,7 @@ const resolveOpenCodeExecutable = async () => {
return candidate; return candidate;
} catch {} } catch {}
} }
return 'opencode'; throw new Error('Git.Zone IDE OpenCode install not found. Run pnpm install so the electron shell dependency opencode-ai is available.');
}; };
const waitForOpenCodeHealth = async (baseUrl: string, username: string, password: string, timeoutMs: number) => { const waitForOpenCodeHealth = async (baseUrl: string, username: string, password: string, timeoutMs: number) => {
@@ -692,6 +789,7 @@ const waitForOpenCodeHealth = async (baseUrl: string, username: string, password
}; };
const disposeLocalOpenCodeRuntime = (runtime: ILocalOpenCodeRuntime) => { const disposeLocalOpenCodeRuntime = (runtime: ILocalOpenCodeRuntime) => {
runtime.eventAbortController?.abort();
runtime.bridge.unregister(runtime.bridgeToken); runtime.bridge.unregister(runtime.bridgeToken);
if (runtime.process.exitCode === null && !runtime.process.killed) { if (runtime.process.exitCode === null && !runtime.process.killed) {
runtime.process.kill('SIGTERM'); runtime.process.kill('SIGTERM');
@@ -748,6 +846,68 @@ const normalizeOptionalPort = (value: number | undefined, label: string) => {
return port; return port;
}; };
const normalizeOptionalLimit = (value: number | undefined, defaultLimit: number) => {
if (value === undefined || value === null) {
return defaultLimit;
}
const limit = Number(value);
if (!Number.isInteger(limit) || limit <= 0) {
return defaultLimit;
}
return Math.min(limit, 500);
};
const requireBoundedText = (value: string | undefined, label: string, maxLength: number) => {
const text = requireTrimmed(value, label);
if (text.length > maxLength) {
throw new Error(`${label} must not exceed ${maxLength} characters.`);
}
return text;
};
const createLocalOpenCodeClient = (runtime: ILocalOpenCodeRuntime) => new plugins.ideOpenCodeBridge.OpenCodeServerClient({
baseUrl: runtime.baseUrl,
username: runtime.username,
password: runtime.password,
});
const startOpenCodeEventStream = (runtime: ILocalOpenCodeRuntime) => {
runtime.eventAbortController?.abort();
const abortController = new AbortController();
runtime.eventAbortController = abortController;
void (async () => {
while (!abortController.signal.aborted && runtime.process.exitCode === null) {
try {
const client = createLocalOpenCodeClient(runtime);
for await (const event of client.events(abortController.signal)) {
if (abortController.signal.aborted) {
return;
}
sendOpenCodeEventToRenderers(runtime.instanceId, event);
}
} catch (error) {
if (abortController.signal.aborted || runtime.process.exitCode !== null) {
return;
}
console.warn(`OpenCode event stream failed for ${runtime.instanceId}: ${error instanceof Error ? error.message : String(error)}`);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
})();
};
const sendOpenCodeEventToRenderers = (instanceId: string, event: plugins.ideOpenCodeBridge.IOpenCodeEvent) => {
const payload = {
instanceId,
event: plugins.ideOpenCodeBridge.sanitizeOpenCodeEventForRenderer(event),
};
for (const window of plugins.electron.BrowserWindow.getAllWindows()) {
if (!window.webContents.isDestroyed()) {
window.webContents.send('gitzone:opencode-event', payload);
}
}
};
const toHostSessionDescriptor = (session: IRemoteHostSession) => ({ const toHostSessionDescriptor = (session: IRemoteHostSession) => ({
id: session.id, id: session.id,
hostAlias: session.target.hostAlias, hostAlias: session.target.hostAlias,
@@ -1085,7 +1245,7 @@ const renderLauncherHtml = () => `<!doctype html>
<title>Git.Zone IDE</title> <title>Git.Zone IDE</title>
<style> <style>
* { box-sizing: border-box; } * { box-sizing: border-box; }
:root { color-scheme: dark; --activity: #333333; --activity-muted: #858585; --activity-hover: #d7d7d7; --side: #252526; --editor: #1e1e1e; --panel: #181818; --border: #3c3c3c; --input: #3c3c3c; --text: #cccccc; --muted: #8f8f8f; --blue: #007acc; --blue-hover: #0e639c; --green: #89d185; --tab: #2d2d2d; --tab-active: #1e1e1e; } :root { color-scheme: dark; --activity: #333333; --activity-muted: #858585; --activity-hover: #d7d7d7; --side: #252526; --editor: #1e1e1e; --panel: #181818; --border: #3c3c3c; --input: #3c3c3c; --text: #cccccc; --muted: #8f8f8f; --blue: #007acc; --blue-hover: #0e639c; --green: #89d185; --tab: #2d2d2d; --tab-active: #1e1e1e; --sidebar-width: 300px; --panel-height: 118px; }
body { margin: 0; overflow: hidden; font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--editor); color: var(--text); } body { margin: 0; overflow: hidden; font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--editor); color: var(--text); }
button, input, select { font: inherit; } button, input, select { font: inherit; }
button { height: 30px; padding: 0 12px; border: 1px solid transparent; color: #ffffff; background: #3a3d41; cursor: pointer; } button { height: 30px; padding: 0 12px; border: 1px solid transparent; color: #ffffff; background: #3a3d41; cursor: pointer; }
@@ -1098,17 +1258,17 @@ const renderLauncherHtml = () => `<!doctype html>
label { display: block; margin: 13px 0 5px; color: #bbbbbb; font-size: 12px; } label { display: block; margin: 13px 0 5px; color: #bbbbbb; font-size: 12px; }
p { color: var(--muted); line-height: 1.45; } p { color: var(--muted); line-height: 1.45; }
[hidden] { display: none !important; } [hidden] { display: none !important; }
.workbench { min-width: 980px; height: 100vh; display: grid; grid-template-rows: 30px 32px minmax(0, 1fr) 118px 22px; } .workbench { min-width: 980px; height: 100vh; display: grid; grid-template-rows: 30px 32px minmax(0, 1fr) 6px var(--panel-height) 22px; }
.titlebar { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 12px; background: #3c3c3c; color: #cccccc; user-select: none; } .titlebar { grid-row: 1; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 12px; background: #3c3c3c; color: #cccccc; user-select: none; }
.titlebar-title { font-size: 12px; } .titlebar-title { font-size: 12px; }
.titlebar-host { color: #d7ba7d; font-size: 12px; } .titlebar-host { color: #d7ba7d; font-size: 12px; }
.project-tabs { display: flex; align-items: end; min-width: 0; overflow-x: auto; background: #252526; border-bottom: 1px solid var(--border); } .project-tabs { grid-row: 2; display: flex; align-items: end; min-width: 0; overflow-x: auto; background: #252526; border-bottom: 1px solid var(--border); }
.project-tab { height: 31px; max-width: 220px; display: flex; align-items: center; gap: 8px; padding: 0 11px; border: 0; border-right: 1px solid var(--border); border-radius: 0; background: var(--tab); color: #c8c8c8; white-space: nowrap; } .project-tab { height: 31px; max-width: 220px; display: flex; align-items: center; gap: 8px; padding: 0 11px; border: 0; border-right: 1px solid var(--border); border-radius: 0; background: var(--tab); color: #c8c8c8; white-space: nowrap; }
.project-tab.active { background: var(--tab-active); color: #ffffff; box-shadow: inset 0 1px 0 var(--blue); } .project-tab.active { background: var(--tab-active); color: #ffffff; box-shadow: inset 0 1px 0 var(--blue); }
.project-tab-close { color: #999999; font-size: 13px; } .project-tab-close { color: #999999; font-size: 13px; }
.project-tab-close:hover { color: #ffffff; } .project-tab-close:hover { color: #ffffff; }
.content { min-height: 0; display: grid; grid-template-columns: 48px 300px minmax(0, 1fr); } .content { grid-row: 3; min-height: 0; display: grid; grid-template-columns: 48px var(--sidebar-width) 6px minmax(0, 1fr); overflow: hidden; }
.activitybar { background: var(--activity); border-right: 1px solid #2b2b2b; display: flex; flex-direction: column; align-items: stretch; justify-content: space-between; padding: 4px 0; } .activitybar { grid-column: 1; background: var(--activity); border-right: 1px solid #2b2b2b; display: flex; flex-direction: column; align-items: stretch; justify-content: space-between; padding: 4px 0; }
.activitybar-group { display: flex; flex-direction: column; align-items: stretch; } .activitybar-group { display: flex; flex-direction: column; align-items: stretch; }
.activity-button { position: relative; width: 47px; height: 48px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 0; background: transparent; color: var(--activity-muted); } .activity-button { position: relative; width: 47px; height: 48px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 0; background: transparent; color: var(--activity-muted); }
.activity-button:hover { background: transparent; color: var(--activity-hover); } .activity-button:hover { background: transparent; color: var(--activity-hover); }
@@ -1117,7 +1277,7 @@ const renderLauncherHtml = () => `<!doctype html>
.activity-button svg { width: 24px; height: 24px; } .activity-button svg { width: 24px; height: 24px; }
.activity-button svg [fill] { fill: currentColor; } .activity-button svg [fill] { fill: currentColor; }
.activity-button-bottom { margin-top: auto; } .activity-button-bottom { margin-top: auto; }
.sidebar { min-width: 0; background: var(--side); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; } .sidebar { grid-column: 2; min-width: 0; background: var(--side); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
.sidebar-panel { min-height: 0; display: flex; flex: 1; flex-direction: column; overflow: hidden; } .sidebar-panel { min-height: 0; display: flex; flex: 1; flex-direction: column; overflow: hidden; }
.sidebar-title { padding: 11px 12px 8px; color: #bbbbbb; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } .sidebar-title { padding: 11px 12px 8px; color: #bbbbbb; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.sidebar-toolbar { display: flex; gap: 6px; padding: 0 10px 10px; } .sidebar-toolbar { display: flex; gap: 6px; padding: 0 10px 10px; }
@@ -1130,6 +1290,22 @@ const renderLauncherHtml = () => `<!doctype html>
.sidebar-form .actions button { flex: 1; } .sidebar-form .actions button { flex: 1; }
.sidebar-note { margin: 0 10px 10px; color: var(--muted); font-size: 12px; line-height: 1.4; } .sidebar-note { margin: 0 10px 10px; color: var(--muted); font-size: 12px; line-height: 1.4; }
.sidebar-output { min-height: 0; flex: 1; margin: 0; border-top: 1px solid var(--border); background: #141414; } .sidebar-output { min-height: 0; flex: 1; margin: 0; border-top: 1px solid var(--border); background: #141414; }
.opencode-session-select { margin: 0 10px 8px; width: calc(100% - 20px); height: 28px; background: #2d2d2d; }
.opencode-permissions { display: flex; flex-direction: column; gap: 8px; padding: 0 10px 8px; }
.opencode-permission { padding: 8px; border: 1px solid #5a3d1a; background: #2a2117; color: #d7ba7d; }
.opencode-permission-title { margin-bottom: 6px; color: #ffffff; font-size: 12px; }
.opencode-permission pre { max-height: 100px; margin: 0 0 8px; padding: 6px; background: #1a1a1a; font-size: 11px; }
.opencode-chat-log { min-height: 0; flex: 1; overflow: auto; padding: 0 10px 8px; border-top: 1px solid var(--border); }
.opencode-message { margin: 8px 0; padding: 8px; border: 1px solid #333333; background: #1f1f1f; }
.opencode-message.user { border-left: 2px solid var(--blue); }
.opencode-message.assistant { border-left: 2px solid var(--green); }
.opencode-message.tool { border-left: 2px solid #d7ba7d; }
.opencode-message-title { margin-bottom: 5px; color: #ffffff; font-size: 11px; font-weight: 700; text-transform: uppercase; }
.opencode-message-text { color: #d4d4d4; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.4; }
.opencode-composer { padding: 8px 10px 10px; border-top: 1px solid var(--border); background: #202020; }
.opencode-composer textarea { width: 100%; min-height: 78px; resize: vertical; padding: 7px 8px; border: 1px solid var(--border); background: #151515; color: #f0f0f0; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; outline: none; }
.opencode-composer textarea:focus { border-color: var(--blue); }
.opencode-composer .actions { margin-top: 7px; }
.host-select { margin: 0 10px 8px; width: calc(100% - 20px); height: 28px; background: #2d2d2d; } .host-select { margin: 0 10px 8px; width: calc(100% - 20px); height: 28px; background: #2d2d2d; }
.host-list, .project-list { overflow: auto; padding-bottom: 8px; } .host-list, .project-list { overflow: auto; padding-bottom: 8px; }
.host-list { max-height: 36%; } .host-list { max-height: 36%; }
@@ -1141,9 +1317,20 @@ const renderLauncherHtml = () => `<!doctype html>
.host-detail, .project-detail { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .host-detail, .project-detail { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty { display: none; margin: 8px 10px; padding: 10px; border: 1px dashed #555555; color: #a6a6a6; background: #1f1f1f; } .empty { display: none; margin: 8px 10px; padding: 10px; border: 1px dashed #555555; color: #a6a6a6; background: #1f1f1f; }
.remote-panel { border-top: 1px solid var(--border); min-height: 0; display: flex; flex-direction: column; } .remote-panel { border-top: 1px solid var(--border); min-height: 0; display: flex; flex-direction: column; }
.main { min-width: 0; min-height: 0; background: var(--editor); display: grid; grid-template-rows: 35px minmax(0, 1fr); } .splitter { position: relative; background: #252526; }
.main-header { display: flex; align-items: center; padding: 0 14px; background: #1f1f1f; border-bottom: 1px solid var(--border); color: #ffffff; } .splitter::before { content: ''; position: absolute; background: transparent; }
.view { min-width: 0; min-height: 0; overflow: auto; padding: 28px 34px 34px; } .splitter:hover, .splitter.active { background: #3a3d41; }
.sidebar-resizer { grid-column: 3; cursor: col-resize; border-right: 1px solid var(--border); }
.sidebar-resizer::before { top: 0; bottom: 0; left: -4px; right: -4px; }
.panel-resizer { grid-row: 4; cursor: row-resize; border-top: 1px solid #2b2b2b; border-bottom: 1px solid var(--border); }
.panel-resizer::before { left: 0; right: 0; top: -4px; bottom: -4px; }
body.resizing { user-select: none; }
.main { grid-column: 4; min-width: 0; min-height: 0; overflow: hidden; background: var(--editor); display: grid; grid-template-rows: 35px minmax(0, 1fr); }
.main-header { grid-row: 1; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 8px 0 14px; background: #1f1f1f; border-bottom: 1px solid var(--border); color: #ffffff; }
.main-header-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main-header-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.main-header-action { height: 24px; padding: 0 9px; font-size: 12px; }
.view { grid-row: 2; min-width: 0; min-height: 0; overflow: auto; padding: 28px 34px 34px; }
.frame-view { padding: 0; overflow: hidden; } .frame-view { padding: 0; overflow: hidden; }
webview { width: 100%; height: 100%; border: 0; background: #111111; } webview { width: 100%; height: 100%; border: 0; background: #111111; }
.welcome { max-width: 840px; } .welcome { max-width: 840px; }
@@ -1159,15 +1346,30 @@ const renderLauncherHtml = () => `<!doctype html>
.project-card-title { color: #ffffff; font-size: 14px; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .project-card-title { color: #ffffff; font-size: 14px; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-card-path { color: var(--muted); font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .project-card-path { color: var(--muted); font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-card button { margin-top: 10px; } .project-card button { margin-top: 10px; }
.panel { background: var(--panel); border-top: 1px solid var(--border); display: grid; grid-template-rows: 28px minmax(0, 1fr); } .panel { grid-row: 5; background: var(--panel); border-top: 1px solid var(--border); display: grid; grid-template-rows: 28px minmax(0, 1fr); }
.panel-title { display: flex; align-items: center; padding: 0 12px; color: #cccccc; font-size: 11px; font-weight: 700; text-transform: uppercase; border-bottom: 1px solid #2a2a2a; } .panel-title { display: flex; align-items: center; padding: 0 12px; color: #cccccc; font-size: 11px; font-weight: 700; text-transform: uppercase; border-bottom: 1px solid #2a2a2a; }
pre { margin: 0; padding: 10px 12px; overflow: auto; white-space: pre-wrap; color: #d4d4d4; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; } pre { margin: 0; padding: 10px 12px; overflow: auto; white-space: pre-wrap; color: #d4d4d4; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; }
.statusbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 10px; background: var(--blue); color: #ffffff; font-size: 12px; } .statusbar { grid-row: 6; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 10px; background: var(--blue); color: #ffffff; font-size: 12px; }
@media (max-width: 860px) { body { overflow: auto; } .workbench { min-width: 0; height: auto; min-height: 100vh; grid-template-rows: 30px auto auto 160px 22px; } .content { grid-template-columns: 1fr; } .activitybar { display: none; } .sidebar { max-height: 420px; } .grid { grid-template-columns: 1fr; } .view { padding: 20px; } } .statusbar-left, .statusbar-right { min-width: 0; display: flex; align-items: center; gap: 10px; }
.statusbar-left { flex: 1; }
.statusbar-right { flex: 0 1 auto; }
.statusbar span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.statusbar-button { display: none; height: 18px; padding: 0 8px; border-color: rgba(255,255,255,0.25); background: rgba(0,0,0,0.18); color: #ffffff; font-size: 11px; }
.statusbar-button:hover { background: rgba(255,255,255,0.16); }
.workbench.main-expanded { grid-template-rows: 0 0 minmax(0, 1fr) 0 0 22px; }
.workbench.main-expanded .content { grid-template-columns: minmax(0, 1fr); }
.workbench.main-expanded .main { grid-column: 1 / -1; }
.workbench.main-expanded .main { grid-template-rows: minmax(0, 1fr); }
.workbench.main-expanded .titlebar, .workbench.main-expanded .project-tabs { display: none; }
.workbench.main-expanded .main-header { display: none; }
.workbench.main-expanded .frame-view { grid-row: 1; }
.workbench.main-expanded .activitybar, .workbench.main-expanded .sidebar, .workbench.main-expanded .sidebar-resizer, .workbench.main-expanded .panel-resizer, .workbench.main-expanded .panel { display: none; }
.workbench.main-expanded .statusbar-button { display: inline-flex; align-items: center; }
@media (max-width: 860px) { body { overflow: auto; } .workbench { min-width: 0; height: auto; min-height: 100vh; grid-template-rows: 30px auto auto 0 160px 22px; } .workbench.main-expanded { grid-template-rows: 30px auto auto 0 0 22px; } .content { grid-template-columns: 1fr; } .activitybar, .sidebar-resizer, .panel-resizer { display: none; } .sidebar { max-height: 420px; } .grid { grid-template-columns: 1fr; } .view { padding: 20px; } }
</style> </style>
</head> </head>
<body> <body>
<main class="workbench"> <main id="workbench" class="workbench">
<div class="titlebar"><span class="titlebar-title">Git.Zone IDE</span><span id="titlebarHost" class="titlebar-host">Not connected</span></div> <div class="titlebar"><span class="titlebar-title">Git.Zone IDE</span><span id="titlebarHost" class="titlebar-host">Not connected</span></div>
<div id="projectTabs" class="project-tabs"></div> <div id="projectTabs" class="project-tabs"></div>
<div class="content"> <div class="content">
@@ -1240,6 +1442,18 @@ const renderLauncherHtml = () => `<!doctype html>
<p class="sidebar-note">OpenCode runs locally with your local provider config. Git.Zone overrides code tools so shell and file operations execute on the selected remote project over SSH.</p> <p class="sidebar-note">OpenCode runs locally with your local provider config. Git.Zone overrides code tools so shell and file operations execute on the selected remote project over SSH.</p>
<div class="sidebar-toolbar"> <div class="sidebar-toolbar">
<button id="showActiveProject" class="secondary">Active Project</button> <button id="showActiveProject" class="secondary">Active Project</button>
<button id="newOpenCodeSession" class="secondary">New Chat</button>
<button id="refreshOpenCode" class="secondary">Refresh</button>
</div>
<select id="opencodeSession" class="opencode-session-select"></select>
<div id="opencodePermissions" class="opencode-permissions"></div>
<div id="opencodeMessages" class="opencode-chat-log"></div>
<div class="opencode-composer">
<textarea id="opencodePrompt" placeholder="Ask OpenCode about this remote project..."></textarea>
<div class="actions">
<button id="sendOpenCodePrompt" class="primary">Send</button>
<button id="abortOpenCode" class="secondary">Stop</button>
</div>
</div> </div>
</section> </section>
<section id="outputPanel" class="sidebar-panel" data-sidebar-panel="output" hidden> <section id="outputPanel" class="sidebar-panel" data-sidebar-panel="output" hidden>
@@ -1260,8 +1474,14 @@ const renderLauncherHtml = () => `<!doctype html>
</div> </div>
</section> </section>
</aside> </aside>
<div id="sidebarResizer" class="splitter sidebar-resizer" role="separator" aria-label="Resize sidebar" aria-orientation="vertical"></div>
<section class="main"> <section class="main">
<div id="mainHeader" class="main-header">Connect to SSH Host</div> <div id="mainHeader" class="main-header">
<span id="mainHeaderTitle" class="main-header-title">Connect to SSH Host</span>
<div class="main-header-actions">
<button id="expandMainPanel" class="secondary main-header-action" type="button" disabled>Expand Theia</button>
</div>
</div>
<div id="connectView" class="view"> <div id="connectView" class="view">
<div class="welcome"> <div class="welcome">
<h1>Connect to Remote Host</h1> <h1>Connect to Remote Host</h1>
@@ -1300,29 +1520,42 @@ const renderLauncherHtml = () => `<!doctype html>
</div> </div>
</section> </section>
</div> </div>
<div id="panelResizer" class="splitter panel-resizer" role="separator" aria-label="Resize output panel" aria-orientation="horizontal"></div>
<div class="panel"> <div class="panel">
<div class="panel-title">Output</div> <div class="panel-title">Output</div>
<pre id="output"></pre> <pre id="output"></pre>
</div> </div>
<div class="statusbar"><span id="statusText">Ready</span><span id="statusRight">OpenSSH</span></div> <div class="statusbar">
<div class="statusbar-left"><span id="statusText">Ready</span></div>
<div class="statusbar-right"><button id="restoreMainPanelFooter" class="statusbar-button" type="button">Restore Shell</button><span id="statusRight">OpenSSH</span></div>
</div>
</main> </main>
<script> <script>
const ideApi = window.gitZoneIde; const ideApi = window.gitZoneIde;
const elements = { const elements = {
workbench: document.getElementById('workbench'),
activityButtons: Array.from(document.querySelectorAll('.activity-button[data-view]')), activityButtons: Array.from(document.querySelectorAll('.activity-button[data-view]')),
sidebarPanels: Array.from(document.querySelectorAll('[data-sidebar-panel]')), sidebarPanels: Array.from(document.querySelectorAll('[data-sidebar-panel]')),
sidebarResizer: document.getElementById('sidebarResizer'),
panelResizer: document.getElementById('panelResizer'),
projectTabs: document.getElementById('projectTabs'), projectTabs: document.getElementById('projectTabs'),
savedHost: document.getElementById('savedHost'), savedHost: document.getElementById('savedHost'),
hostList: document.getElementById('hostList'), hostList: document.getElementById('hostList'),
emptyState: document.getElementById('emptyState'), emptyState: document.getElementById('emptyState'),
explorerHostStatus: document.getElementById('explorerHostStatus'), explorerHostStatus: document.getElementById('explorerHostStatus'),
opencodeStatus: document.getElementById('opencodeStatus'), opencodeStatus: document.getElementById('opencodeStatus'),
opencodeSession: document.getElementById('opencodeSession'),
opencodePermissions: document.getElementById('opencodePermissions'),
opencodeMessages: document.getElementById('opencodeMessages'),
opencodePrompt: document.getElementById('opencodePrompt'),
projectList: document.getElementById('projectList'), projectList: document.getElementById('projectList'),
projectEmptyState: document.getElementById('projectEmptyState'), projectEmptyState: document.getElementById('projectEmptyState'),
projectCards: document.getElementById('projectCards'), projectCards: document.getElementById('projectCards'),
configPath: document.getElementById('configPath'), configPath: document.getElementById('configPath'),
titlebarHost: document.getElementById('titlebarHost'), titlebarHost: document.getElementById('titlebarHost'),
mainHeader: document.getElementById('mainHeader'), mainHeaderTitle: document.getElementById('mainHeaderTitle'),
expandMainPanel: document.getElementById('expandMainPanel'),
restoreMainPanelFooter: document.getElementById('restoreMainPanelFooter'),
connectView: document.getElementById('connectView'), connectView: document.getElementById('connectView'),
dashboardView: document.getElementById('dashboardView'), dashboardView: document.getElementById('dashboardView'),
frameView: document.getElementById('frameView'), frameView: document.getElementById('frameView'),
@@ -1347,6 +1580,23 @@ const renderLauncherHtml = () => `<!doctype html>
let openTabs = []; let openTabs = [];
let activeTabId = 'dashboard'; let activeTabId = 'dashboard';
let currentActivityView = 'hosts'; let currentActivityView = 'hosts';
let projectFrameDomReady = false;
let opencodeState = {
instanceId: undefined,
sessions: [],
activeSessionId: undefined,
messages: [],
permissions: [],
loading: false,
};
let opencodeRefreshTimer = undefined;
const layoutStorageKey = 'gitzone-ide-layout-v1';
const defaultLayoutState = {
sidebarWidth: 300,
panelHeight: 118,
mainExpanded: false,
};
let layoutState = { ...defaultLayoutState };
const optionalNumber = (value) => value ? Number(value) : undefined; const optionalNumber = (value) => value ? Number(value) : undefined;
const selectedHost = () => hosts.find((host) => host.alias === elements.savedHost.value); const selectedHost = () => hosts.find((host) => host.alias === elements.savedHost.value);
@@ -1369,6 +1619,9 @@ const renderLauncherHtml = () => `<!doctype html>
if (ideApi.onConnectProgress) { if (ideApi.onConnectProgress) {
ideApi.onConnectProgress((message) => appendOutput(String(message))); ideApi.onConnectProgress((message) => appendOutput(String(message)));
} }
if (ideApi.openCode?.onEvent) {
ideApi.openCode.onEvent((payload) => handleOpenCodeEvent(payload));
}
const formPayload = () => ({ const formPayload = () => ({
hostAlias: elements.alias.value.trim() || undefined, hostAlias: elements.alias.value.trim() || undefined,
alias: elements.alias.value.trim() || undefined, alias: elements.alias.value.trim() || undefined,
@@ -1379,7 +1632,359 @@ const renderLauncherHtml = () => `<!doctype html>
proxyJump: elements.proxyJump.value.trim() || undefined, proxyJump: elements.proxyJump.value.trim() || undefined,
}); });
const recordId = (record) => record?.id || record?.ID || record?.sessionID || record?.sessionId || record?.permissionID || record?.permissionId;
const sessionTitle = (session) => session?.title || session?.name || recordId(session) || 'Session';
const activeOpenCodeTab = () => {
const tab = activeProjectTab();
return tab?.openCode?.status === 'ready' ? tab : undefined;
};
const flattenText = (value) => {
if (value === undefined || value === null) return '';
if (typeof value === 'string') return value;
if (Array.isArray(value)) return value.map(flattenText).filter(Boolean).join('\\n');
if (typeof value === 'object') {
if (typeof value.text === 'string') return value.text;
if (typeof value.content === 'string') return value.content;
if (typeof value.output === 'string') return value.output;
if (Array.isArray(value.parts)) return flattenText(value.parts);
return JSON.stringify(value, undefined, 2);
}
return String(value);
};
const messageRole = (message) => {
const info = message?.info || message;
return String(info?.role || info?.type || info?.author || 'message').toLowerCase();
};
const messageText = (message) => {
const parts = message?.parts || message?.info?.parts || [];
if (Array.isArray(parts) && parts.length) {
return parts.map((part) => {
const type = part?.type ? String(part.type) : 'part';
const text = flattenText(part?.text ?? part?.content ?? part?.output ?? part?.message ?? part);
return type === 'text' ? text : '[' + type + ']\\n' + text;
}).filter(Boolean).join('\\n\\n');
}
return flattenText(message?.text ?? message?.content ?? message?.message ?? message);
};
const eventPayload = (event) => {
const data = event?.data;
return data && typeof data === 'object' && data.properties && typeof data.properties === 'object' ? data.properties : data || {};
};
const permissionIdFromPayload = (data) => data.requestID || data.requestId || data.permissionID || data.permissionId || data.id || data.permission?.id;
const sessionIdFromPayload = (data) => data.sessionID || data.sessionId || data.session?.id;
const setOpenCodeDisabled = (disabled) => {
elements.opencodeSession.disabled = disabled;
elements.opencodePrompt.disabled = disabled;
document.getElementById('sendOpenCodePrompt').disabled = disabled;
document.getElementById('abortOpenCode').disabled = disabled;
document.getElementById('newOpenCodeSession').disabled = disabled;
document.getElementById('refreshOpenCode').disabled = disabled;
};
const renderOpenCodeSessions = () => {
elements.opencodeSession.replaceChildren();
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = opencodeState.sessions.length ? 'Select session' : 'No sessions yet';
elements.opencodeSession.appendChild(emptyOption);
for (const session of opencodeState.sessions) {
const option = document.createElement('option');
option.value = recordId(session) || '';
option.textContent = sessionTitle(session);
elements.opencodeSession.appendChild(option);
}
elements.opencodeSession.value = opencodeState.activeSessionId || '';
};
const renderOpenCodeMessages = () => {
elements.opencodeMessages.replaceChildren();
if (!opencodeState.messages.length) {
const empty = document.createElement('div');
empty.className = 'empty';
empty.style.display = 'block';
empty.textContent = opencodeState.activeSessionId ? 'No messages in this session yet.' : 'Create or select a session to chat.';
elements.opencodeMessages.appendChild(empty);
return;
}
for (const message of opencodeState.messages) {
const role = messageRole(message);
const container = document.createElement('div');
container.className = 'opencode-message ' + (role.includes('user') ? 'user' : role.includes('assistant') ? 'assistant' : role.includes('tool') ? 'tool' : '');
const title = document.createElement('div');
title.className = 'opencode-message-title';
title.textContent = role;
const text = document.createElement('div');
text.className = 'opencode-message-text';
text.textContent = messageText(message) || '(empty)';
container.append(title, text);
elements.opencodeMessages.appendChild(container);
}
elements.opencodeMessages.scrollTop = elements.opencodeMessages.scrollHeight;
};
const renderOpenCodePermissions = () => {
elements.opencodePermissions.replaceChildren();
for (const permission of opencodeState.permissions) {
const data = eventPayload(permission);
const card = document.createElement('div');
card.className = 'opencode-permission';
const title = document.createElement('div');
title.className = 'opencode-permission-title';
title.textContent = data.title || data.permission || 'Permission requested';
const body = document.createElement('pre');
body.textContent = JSON.stringify(data, undefined, 2);
const actions = document.createElement('div');
actions.className = 'actions';
const once = document.createElement('button');
once.className = 'primary';
once.textContent = 'Allow Once';
once.addEventListener('click', () => respondOpenCodePermission(permission, 'once'));
const always = document.createElement('button');
always.className = 'secondary';
always.textContent = 'Always';
always.addEventListener('click', () => respondOpenCodePermission(permission, 'always'));
const reject = document.createElement('button');
reject.className = 'secondary';
reject.textContent = 'Reject';
reject.addEventListener('click', () => respondOpenCodePermission(permission, 'reject'));
actions.append(once, always, reject);
card.append(title, body, actions);
elements.opencodePermissions.appendChild(card);
}
};
const renderOpenCodePanel = () => {
const tab = activeOpenCodeTab();
renderActivityPanelState();
renderOpenCodeSessions();
renderOpenCodeMessages();
renderOpenCodePermissions();
setOpenCodeDisabled(!tab || opencodeState.loading || !opencodeState.activeSessionId);
document.getElementById('newOpenCodeSession').disabled = !tab || opencodeState.loading;
document.getElementById('refreshOpenCode').disabled = !tab || opencodeState.loading;
};
const resetOpenCodeState = (instanceId) => {
opencodeState = {
instanceId,
sessions: [],
activeSessionId: undefined,
messages: [],
permissions: [],
loading: false,
};
};
const refreshOpenCodeForActiveProject = async (autoCreate) => {
const tab = activeOpenCodeTab();
if (!tab) {
resetOpenCodeState(undefined);
renderOpenCodePanel();
return;
}
if (opencodeState.instanceId !== tab.id) {
resetOpenCodeState(tab.id);
}
opencodeState.loading = true;
renderOpenCodePanel();
try {
const sessions = await ideApi.openCode.sessions({ instanceId: tab.id });
opencodeState.sessions = Array.isArray(sessions) ? sessions : [];
if (!opencodeState.activeSessionId || !opencodeState.sessions.some((session) => recordId(session) === opencodeState.activeSessionId)) {
opencodeState.activeSessionId = recordId(opencodeState.sessions[0]);
}
if (autoCreate && !opencodeState.activeSessionId) {
const session = await ideApi.openCode.createSession({ instanceId: tab.id, title: 'Git.Zone IDE Session' });
opencodeState.sessions = [session].filter(Boolean);
opencodeState.activeSessionId = recordId(session);
}
await loadOpenCodeMessages();
} catch (error) {
appendOutput(error.stack || String(error));
} finally {
opencodeState.loading = false;
renderOpenCodePanel();
}
};
const loadOpenCodeMessages = async () => {
const tab = activeOpenCodeTab();
if (!tab || !opencodeState.activeSessionId) {
opencodeState.messages = [];
return;
}
const messages = await ideApi.openCode.messages({
instanceId: tab.id,
sessionId: opencodeState.activeSessionId,
limit: 100,
});
opencodeState.messages = Array.isArray(messages) ? messages : [];
};
const createOpenCodeSession = async () => {
const tab = activeOpenCodeTab();
if (!tab) return;
opencodeState.loading = true;
renderOpenCodePanel();
try {
const session = await ideApi.openCode.createSession({ instanceId: tab.id, title: 'Git.Zone IDE Session' });
opencodeState.activeSessionId = recordId(session);
await refreshOpenCodeForActiveProject(false);
} catch (error) {
appendOutput(error.stack || String(error));
} finally {
opencodeState.loading = false;
renderOpenCodePanel();
}
};
const sendOpenCodePrompt = async () => {
const tab = activeOpenCodeTab();
const text = elements.opencodePrompt.value.trim();
if (!tab || !text) return;
if (!opencodeState.activeSessionId) {
await createOpenCodeSession();
}
if (!opencodeState.activeSessionId) return;
elements.opencodePrompt.value = '';
opencodeState.loading = true;
renderOpenCodePanel();
try {
await ideApi.openCode.prompt({ instanceId: tab.id, sessionId: opencodeState.activeSessionId, text });
await loadOpenCodeMessages();
} catch (error) {
appendOutput(error.stack || String(error));
} finally {
opencodeState.loading = false;
renderOpenCodePanel();
}
};
const abortOpenCode = async () => {
const tab = activeOpenCodeTab();
if (!tab || !opencodeState.activeSessionId) return;
try {
await ideApi.openCode.abort({ instanceId: tab.id, sessionId: opencodeState.activeSessionId });
} catch (error) {
appendOutput(error.stack || String(error));
}
};
const respondOpenCodePermission = async (permission, response) => {
const tab = activeOpenCodeTab();
const data = eventPayload(permission);
const permissionId = permissionIdFromPayload(data);
const sessionId = sessionIdFromPayload(data) || opencodeState.activeSessionId;
if (!tab || !permissionId || !sessionId) return;
try {
await ideApi.openCode.respondToPermission({ instanceId: tab.id, sessionId, permissionId, response, remember: response === 'always' });
opencodeState.permissions = opencodeState.permissions.filter((candidate) => candidate !== permission);
renderOpenCodePanel();
} catch (error) {
appendOutput(error.stack || String(error));
}
};
const scheduleOpenCodeRefresh = () => {
if (opencodeRefreshTimer) return;
opencodeRefreshTimer = window.setTimeout(async () => {
opencodeRefreshTimer = undefined;
await refreshOpenCodeForActiveProject(false);
}, 600);
};
const handleOpenCodeEvent = (payload) => {
if (!payload || payload.instanceId !== opencodeState.instanceId) return;
const event = payload.event || {};
const type = String(event.type || '');
if (type === 'permission.asked' || type === 'permission.updated') {
const data = eventPayload(event);
const permissionId = permissionIdFromPayload(data);
opencodeState.permissions = permissionId
? opencodeState.permissions.filter((candidate) => permissionIdFromPayload(eventPayload(candidate)) !== permissionId).concat(event)
: opencodeState.permissions.concat(event);
renderOpenCodePanel();
return;
}
if (type === 'permission.replied') {
const data = eventPayload(event);
const permissionId = permissionIdFromPayload(data);
opencodeState.permissions = permissionId
? opencodeState.permissions.filter((candidate) => {
return permissionIdFromPayload(eventPayload(candidate)) !== permissionId;
})
: [];
renderOpenCodePanel();
return;
}
if (type.startsWith('message.') || type.startsWith('session.') || type.startsWith('tool.') || type === 'server.connected') {
scheduleOpenCodeRefresh();
}
};
const activeProjectTab = () => openTabs.find((candidate) => candidate.projectId === activeTabId); const activeProjectTab = () => openTabs.find((candidate) => candidate.projectId === activeTabId);
const isTheiaFrameShowing = () => Boolean(activeProjectTab() && !elements.frameView.hidden && projectFrameDomReady && elements.projectFrame.src);
const clampNumber = (value, min, max) => Math.min(max, Math.max(min, value));
const loadLayoutState = () => {
try {
const stored = JSON.parse(localStorage.getItem(layoutStorageKey) || '{}');
layoutState = {
sidebarWidth: clampNumber(Number(stored.sidebarWidth) || defaultLayoutState.sidebarWidth, 180, 560),
panelHeight: clampNumber(Number(stored.panelHeight) || defaultLayoutState.panelHeight, 52, 360),
mainExpanded: Boolean(stored.mainExpanded),
};
} catch {
layoutState = { ...defaultLayoutState };
}
};
const saveLayoutState = () => {
try {
localStorage.setItem(layoutStorageKey, JSON.stringify(layoutState));
} catch {}
};
const applyLayoutState = () => {
elements.workbench.style.setProperty('--sidebar-width', layoutState.sidebarWidth + 'px');
elements.workbench.style.setProperty('--panel-height', layoutState.panelHeight + 'px');
elements.workbench.classList.toggle('main-expanded', layoutState.mainExpanded);
};
const syncProjectFrameLayout = () => {
if (elements.frameView.hidden || !projectFrameDomReady) return;
if (typeof elements.projectFrame.executeJavaScript === 'function') {
try {
elements.projectFrame.executeJavaScript('window.dispatchEvent(new Event("resize"));', false).catch(() => undefined);
} catch {
projectFrameDomReady = false;
}
}
};
const scheduleProjectFrameLayoutSync = () => {
for (const delay of [0, 50, 200]) {
window.setTimeout(() => requestAnimationFrame(syncProjectFrameLayout), delay);
}
};
const renderMainPanelControls = () => {
const canExpandTheia = isTheiaFrameShowing();
elements.expandMainPanel.disabled = !canExpandTheia;
elements.expandMainPanel.textContent = layoutState.mainExpanded ? 'Restore Shell' : 'Expand Theia';
elements.restoreMainPanelFooter.disabled = !layoutState.mainExpanded;
};
const setMainExpanded = (expanded) => {
if (expanded && !isTheiaFrameShowing()) return;
layoutState.mainExpanded = Boolean(expanded);
saveLayoutState();
applyLayoutState();
renderMainPanelControls();
scheduleProjectFrameLayoutSync();
};
const beginDragResize = (event, splitter, axis, startValue, updateValue) => {
if (layoutState.mainExpanded) return;
event.preventDefault();
const startPosition = axis === 'x' ? event.clientX : event.clientY;
splitter.classList.add('active');
document.body.classList.add('resizing');
const onPointerMove = (moveEvent) => {
const currentPosition = axis === 'x' ? moveEvent.clientX : moveEvent.clientY;
updateValue(startValue, currentPosition - startPosition);
applyLayoutState();
syncProjectFrameLayout();
};
const onPointerUp = () => {
splitter.classList.remove('active');
document.body.classList.remove('resizing');
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
saveLayoutState();
};
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp, { once: true });
};
const renderActivityPanelState = () => { const renderActivityPanelState = () => {
elements.explorerHostStatus.textContent = connection elements.explorerHostStatus.textContent = connection
? 'Connected to ' + connection.hostAlias + '. ' + projects.length + ' remote project' + (projects.length === 1 ? '' : 's') + ' registered.' ? 'Connected to ' + connection.hostAlias + '. ' + projects.length + ' remote project' + (projects.length === 1 ? '' : 's') + ' registered.'
@@ -1520,17 +2125,27 @@ const renderLauncherHtml = () => `<!doctype html>
elements.dashboardView.hidden = tabId !== 'dashboard' || !connection; elements.dashboardView.hidden = tabId !== 'dashboard' || !connection;
elements.frameView.hidden = !tab; elements.frameView.hidden = !tab;
if (tab) { if (tab) {
projectFrameDomReady = false;
elements.projectFrame.src = tab.url; elements.projectFrame.src = tab.url;
elements.mainHeader.textContent = tab.title + ' - ' + tab.path; elements.mainHeaderTitle.textContent = tab.title + ' - ' + tab.path;
scheduleProjectFrameLayoutSync();
} else if (connection) { } else if (connection) {
elements.projectFrame.removeAttribute('src'); elements.projectFrame.removeAttribute('src');
elements.mainHeader.textContent = 'Remote Project Dashboard'; elements.mainHeaderTitle.textContent = 'Remote Project Dashboard';
if (layoutState.mainExpanded) {
setMainExpanded(false);
}
} else { } else {
elements.projectFrame.removeAttribute('src'); elements.projectFrame.removeAttribute('src');
elements.mainHeader.textContent = 'Connect to SSH Host'; elements.mainHeaderTitle.textContent = 'Connect to SSH Host';
if (layoutState.mainExpanded) {
setMainExpanded(false);
}
} }
renderMainPanelControls();
renderTabs(); renderTabs();
renderProjects(); renderProjects();
void refreshOpenCodeForActiveProject(!!tab);
}; };
const closeTab = (tabId) => { const closeTab = (tabId) => {
@@ -1608,6 +2223,71 @@ const renderLauncherHtml = () => `<!doctype html>
activateActivityView('hosts'); activateActivityView('hosts');
} }
}); });
elements.expandMainPanel.addEventListener('click', () => {
setMainExpanded(!layoutState.mainExpanded);
});
elements.restoreMainPanelFooter.addEventListener('click', () => {
setMainExpanded(false);
});
elements.projectFrame.addEventListener('did-stop-loading', () => {
projectFrameDomReady = true;
scheduleProjectFrameLayoutSync();
renderMainPanelControls();
});
elements.projectFrame.addEventListener('dom-ready', () => {
projectFrameDomReady = true;
scheduleProjectFrameLayoutSync();
renderMainPanelControls();
});
window.addEventListener('resize', () => {
scheduleProjectFrameLayoutSync();
});
if (typeof ResizeObserver === 'function') {
new ResizeObserver(() => scheduleProjectFrameLayoutSync()).observe(elements.frameView);
}
elements.sidebarResizer.addEventListener('pointerdown', (event) => {
beginDragResize(event, elements.sidebarResizer, 'x', layoutState.sidebarWidth, (startWidth, delta) => {
layoutState.sidebarWidth = clampNumber(startWidth + delta, 180, Math.max(220, window.innerWidth - 420));
});
scheduleProjectFrameLayoutSync();
});
elements.panelResizer.addEventListener('pointerdown', (event) => {
beginDragResize(event, elements.panelResizer, 'y', layoutState.panelHeight, (startHeight, delta) => {
layoutState.panelHeight = clampNumber(startHeight - delta, 52, Math.max(80, window.innerHeight - 220));
});
scheduleProjectFrameLayoutSync();
});
elements.opencodeSession.addEventListener('change', async () => {
opencodeState.activeSessionId = elements.opencodeSession.value || undefined;
opencodeState.loading = true;
renderOpenCodePanel();
try {
await loadOpenCodeMessages();
} catch (error) {
appendOutput(error.stack || String(error));
} finally {
opencodeState.loading = false;
renderOpenCodePanel();
}
});
document.getElementById('newOpenCodeSession').addEventListener('click', () => {
void createOpenCodeSession();
});
document.getElementById('refreshOpenCode').addEventListener('click', () => {
void refreshOpenCodeForActiveProject(false);
});
document.getElementById('sendOpenCodePrompt').addEventListener('click', () => {
void sendOpenCodePrompt();
});
document.getElementById('abortOpenCode').addEventListener('click', () => {
void abortOpenCode();
});
elements.opencodePrompt.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
void sendOpenCodePrompt();
}
});
document.getElementById('clearOutput').addEventListener('click', () => { document.getElementById('clearOutput').addEventListener('click', () => {
elements.output.textContent = ''; elements.output.textContent = '';
elements.statusText.textContent = 'Ready'; elements.statusText.textContent = 'Ready';
@@ -1684,6 +2364,13 @@ const renderLauncherHtml = () => `<!doctype html>
} }
}); });
loadLayoutState();
if (layoutState.mainExpanded && !activeProjectTab()) {
layoutState.mainExpanded = false;
saveLayoutState();
}
applyLayoutState();
renderMainPanelControls();
renderTabs(); renderTabs();
renderProjects(); renderProjects();
activateActivityView('hosts'); activateActivityView('hosts');
-1
View File
@@ -9,7 +9,6 @@
"watch": "pnpm run rebuild && theia build --watch --mode development" "watch": "pnpm run rebuild && theia build --watch --mode development"
}, },
"dependencies": { "dependencies": {
"@git.zone/ide-extension-opencode": "workspace:*",
"@git.zone/ide-extension-product": "workspace:*", "@git.zone/ide-extension-product": "workspace:*",
"@git.zone/ide-extension-remote": "workspace:*", "@git.zone/ide-extension-remote": "workspace:*",
"@theia/core": "1.71.0", "@theia/core": "1.71.0",
@@ -16,10 +16,6 @@ globalThis.extensionInfo = [
"name": "@theia/core", "name": "@theia/core",
"version": "1.71.0" "version": "1.71.0"
}, },
{
"name": "@git.zone/ide-extension-opencode",
"version": "0.1.0"
},
{ {
"name": "@git.zone/ide-extension-product", "name": "@git.zone/ide-extension-product",
"version": "0.1.0" "version": "0.1.0"
@@ -61,7 +61,6 @@ module.exports = async (port, host, argv) => {
await load(require('@theia/core/lib/node/i18n/i18n-backend-module')); await load(require('@theia/core/lib/node/i18n/i18n-backend-module'));
await load(require('@theia/core/lib/node/hosting/backend-hosting-module')); await load(require('@theia/core/lib/node/hosting/backend-hosting-module'));
await load(require('@theia/core/lib/node/request/backend-request-module')); await load(require('@theia/core/lib/node/request/backend-request-module'));
await load(require('@git.zone/ide-extension-opencode/lib/node/gitzone-opencode-backend-module'));
await load(require('@git.zone/ide-extension-remote/lib/node/gitzone-remote-backend-module')); await load(require('@git.zone/ide-extension-remote/lib/node/gitzone-remote-backend-module'));
await load(require('@theia/editor/lib/node/editor-backend-module')); await load(require('@theia/editor/lib/node/editor-backend-module'));
await load(require('@theia/filesystem/lib/node/filesystem-backend-module')); await load(require('@theia/filesystem/lib/node/filesystem-backend-module'));
@@ -81,7 +81,6 @@ module.exports = (async () => {
await load(container, import('@theia/core/lib/browser/window/browser-window-module')); await load(container, import('@theia/core/lib/browser/window/browser-window-module'));
await load(container, import('@theia/core/lib/browser/keyboard/browser-keyboard-module')); await load(container, import('@theia/core/lib/browser/keyboard/browser-keyboard-module'));
await load(container, import('@theia/core/lib/browser/request/browser-request-module')); await load(container, import('@theia/core/lib/browser/request/browser-request-module'));
await load(container, import('@git.zone/ide-extension-opencode/lib/browser/gitzone-opencode-frontend-module'));
await load(container, import('@git.zone/ide-extension-product/lib/browser/gitzone-product-frontend-module')); await load(container, import('@git.zone/ide-extension-product/lib/browser/gitzone-product-frontend-module'));
await load(container, import('@git.zone/ide-extension-remote/lib/browser/gitzone-remote-frontend-module')); await load(container, import('@git.zone/ide-extension-remote/lib/browser/gitzone-remote-frontend-module'));
await load(container, import('@theia/variable-resolver/lib/browser/variable-resolver-frontend-module')); await load(container, import('@theia/variable-resolver/lib/browser/variable-resolver-frontend-module'));
+4 -2
View File
@@ -12,8 +12,10 @@ The remote server is a Theia browser application installed under `~/.git.zone/id
## OpenCode Integration ## OpenCode Integration
The `@git.zone/ide-extension-opencode` backend starts or connects to `opencode serve` in the remote workspace. The Theia frontend talks to that backend over Theia JSON-RPC. The browser never receives the OpenCode server password and never talks to OpenCode HTTP directly. The Electron shell starts a local `opencode serve` runtime for each opened project. The native shell owns OpenCode sessions, messages, permissions, and chat UI. Git.Zone writes OpenCode tool overrides into that local runtime so shell, file, search, and patch operations are forwarded over SSH and execute in the selected remote workspace.
Theia does not host OpenCode chat or OpenCode sessions. Any future Theia-facing tool should be an intellisense/context provider for editor and language-server state, not another OpenCode transport.
## Execution Boundary ## Execution Boundary
Files, terminal commands, Git, language servers, builds, tests, Git.Zone commands, and OpenCode tools all run on the remote SSH host. The local machine only displays UI and maintains SSH transport. Files, terminal commands, Git, language servers, builds, tests, Git.Zone commands, and bridged OpenCode code tools all run on the remote SSH host. The local machine displays UI, runs the local OpenCode provider process, and maintains SSH transport.
+14
View File
@@ -53,6 +53,13 @@ export interface IOpenCodeEvent {
raw: string; raw: string;
} }
export interface IRendererOpenCodeEvent {
type: string;
id?: string;
retry?: number;
data?: unknown;
}
export class OpenCodeHttpError extends Error { export class OpenCodeHttpError extends Error {
constructor( constructor(
message: string, message: string,
@@ -476,6 +483,13 @@ export const parseServerSentEvent = (raw: string): IOpenCodeEvent | undefined =>
return { type, id, retry, data, raw }; return { type, id, retry, data, raw };
}; };
export const sanitizeOpenCodeEventForRenderer = (event: IOpenCodeEvent): IRendererOpenCodeEvent => ({
type: event.type,
id: event.id,
retry: event.retry,
data: event.data,
});
const parseJsonIfPossible = (value: string) => { const parseJsonIfPossible = (value: string) => {
if (!value) { if (!value) {
return undefined; return undefined;
+118 -12
View File
@@ -47,6 +47,9 @@ importers:
electron: electron:
specifier: ^42.0.1 specifier: ^42.0.1
version: 42.0.1 version: 42.0.1
opencode-ai:
specifier: 1.14.48
version: 1.14.48
devDependencies: devDependencies:
electron-builder: electron-builder:
specifier: ^26.8.1 specifier: ^26.8.1
@@ -54,9 +57,6 @@ importers:
applications/remote-theia: applications/remote-theia:
dependencies: dependencies:
'@git.zone/ide-extension-opencode':
specifier: workspace:*
version: link:../../theia-extensions/gitzone-opencode
'@git.zone/ide-extension-product': '@git.zone/ide-extension-product':
specifier: workspace:* specifier: workspace:*
version: link:../../theia-extensions/gitzone-product version: link:../../theia-extensions/gitzone-product
@@ -147,15 +147,6 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../protocol version: link:../protocol
theia-extensions/gitzone-opencode:
dependencies:
'@git.zone/ide-opencode-bridge':
specifier: workspace:*
version: link:../../packages/opencode-bridge
'@theia/core':
specifier: 1.71.0
version: 1.71.0
theia-extensions/gitzone-product: theia-extensions/gitzone-product:
dependencies: dependencies:
'@theia/core': '@theia/core':
@@ -5621,6 +5612,70 @@ packages:
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
opencode-ai@1.14.48:
resolution: {integrity: sha512-65ogCJao8ujS4gFP64QuStv/h+qII30xNiARGsu9ai8Uoj+a/m6gz8yhNC9oz1dZm7PF6n15iIpB/LzCXBrhZQ==}
hasBin: true
opencode-darwin-arm64@1.14.48:
resolution: {integrity: sha512-QF05WtuPVnXCkvBHdDSuIhHVgYb/f10HO2mRlUbbBWStbPedLWwOKC4YKUhpsrLAVBEFLcL/xh7RtPM70+0TZQ==}
cpu: [arm64]
os: [darwin]
opencode-darwin-x64-baseline@1.14.48:
resolution: {integrity: sha512-F3p1GRuPR+HKDVIGn/uS9N0685BQ1msOOZsYHiAc4Qe22ZhZGrnMveg4CZwDseAQxoFL5n29i9J7iw/QORmO/g==}
cpu: [x64]
os: [darwin]
opencode-darwin-x64@1.14.48:
resolution: {integrity: sha512-Yx0/opXz7cdne1Xi77TNTIwWpYSwv+T32PdEcyqrcgEx0NR5f4S4kEsydmKsV7lzFRSkqRXniwvLj+8BPJEwNA==}
cpu: [x64]
os: [darwin]
opencode-linux-arm64-musl@1.14.48:
resolution: {integrity: sha512-w+wY4ROlq6lpL+SPYLYHBA6k+twVU9kurBGt+6irf4ZA5BCkhva7mgRET4NBxheUKEPSEQknq3Umi2Ltab4G+w==}
cpu: [arm64]
os: [linux]
opencode-linux-arm64@1.14.48:
resolution: {integrity: sha512-DPZUi4IErlM/oXbNQKOiAqlmIij322sGUewNkvn+UptZtoqMVjtM8UQc6RkfoSQtAhk9zfdXTRjmajXpSSPJTA==}
cpu: [arm64]
os: [linux]
opencode-linux-x64-baseline-musl@1.14.48:
resolution: {integrity: sha512-92icmlOdDHLm0ityztGHGlgv6OiqQTdXYZRsPUs17IcHlkxLUT2b5O8HUpHk/xtMwyJEleYpV3E5EQz4vP5HVw==}
cpu: [x64]
os: [linux]
opencode-linux-x64-baseline@1.14.48:
resolution: {integrity: sha512-I8KoLTSpCYkChdosh2iFfXO0zwPuAp7ZbPZyzPN9R0jTW95EstO5qYPTLs8F+sZJiM+oUdxOEpgIVkGF6JMzAg==}
cpu: [x64]
os: [linux]
opencode-linux-x64-musl@1.14.48:
resolution: {integrity: sha512-h2QPam8t++TEphKm8zroxfuDZk8oAF40aKFxH753XrINfCAyRikOb/hNfLJd0J3oAcolxMbFHjr6c35zyJbzHQ==}
cpu: [x64]
os: [linux]
opencode-linux-x64@1.14.48:
resolution: {integrity: sha512-WLAABtQD2Zr1+7zfGRcNYdAKYtr2EuMY6RwjoNRAqHzQslOlItZhDpwdqg03YFe0zrQhg0IlK6iGuplF1ertdw==}
cpu: [x64]
os: [linux]
opencode-windows-arm64@1.14.48:
resolution: {integrity: sha512-WqcJHoxI3e06So6g83tKuyAGYXHp++/urLtojw78HDhV0RlO9Sthbv19YRu9gqp/GLYNqNChU5JLjVYFmoegkg==}
cpu: [arm64]
os: [win32]
opencode-windows-x64-baseline@1.14.48:
resolution: {integrity: sha512-3RC6Pq/9yJn4jnWjOdDbJPJgIoMXRmWr4AytNeBwi7NifTBGmGnqxU+mFRWYJxJmCIvyWxHw/iVfEUtoej7/JQ==}
cpu: [x64]
os: [win32]
opencode-windows-x64@1.14.48:
resolution: {integrity: sha512-dq5glnTtVjd5sJJcQWpcSHTHj4x9hKsBTFSvGV7tV+FoBTQFsnPtCFqt6Wmo/s0D0VYQsMz/syY1YGtrA+O3pA==}
cpu: [x64]
os: [win32]
opener@1.5.2: opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true hasBin: true
@@ -15315,6 +15370,57 @@ snapshots:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl: 2.2.0 is-wsl: 2.2.0
opencode-ai@1.14.48:
optionalDependencies:
opencode-darwin-arm64: 1.14.48
opencode-darwin-x64: 1.14.48
opencode-darwin-x64-baseline: 1.14.48
opencode-linux-arm64: 1.14.48
opencode-linux-arm64-musl: 1.14.48
opencode-linux-x64: 1.14.48
opencode-linux-x64-baseline: 1.14.48
opencode-linux-x64-baseline-musl: 1.14.48
opencode-linux-x64-musl: 1.14.48
opencode-windows-arm64: 1.14.48
opencode-windows-x64: 1.14.48
opencode-windows-x64-baseline: 1.14.48
opencode-darwin-arm64@1.14.48:
optional: true
opencode-darwin-x64-baseline@1.14.48:
optional: true
opencode-darwin-x64@1.14.48:
optional: true
opencode-linux-arm64-musl@1.14.48:
optional: true
opencode-linux-arm64@1.14.48:
optional: true
opencode-linux-x64-baseline-musl@1.14.48:
optional: true
opencode-linux-x64-baseline@1.14.48:
optional: true
opencode-linux-x64-musl@1.14.48:
optional: true
opencode-linux-x64@1.14.48:
optional: true
opencode-windows-arm64@1.14.48:
optional: true
opencode-windows-x64-baseline@1.14.48:
optional: true
opencode-windows-x64@1.14.48:
optional: true
opener@1.5.2: {} opener@1.5.2: {}
opfs-worker@1.3.1(typescript@6.0.3): opfs-worker@1.3.1(typescript@6.0.3):
+2 -2
View File
@@ -1,8 +1,8 @@
# Git.Zone IDE # Git.Zone IDE
Git.Zone IDE is a remote-first desktop IDE based on Eclipse Theia, Electron, SSH, and OpenCode server. Git.Zone IDE is a remote-first desktop IDE based on Eclipse Theia, Electron, SSH, and OpenCode.
The local Electron shell manages SSH sessions and tunnels. The remote host runs the Theia backend and OpenCode server inside the selected workspace, so files, terminals, Git, language servers, tests, and AI agent actions all execute where the code lives. The local Electron shell manages SSH sessions, tunnels, and the native OpenCode chat runtime. The remote host runs the Theia backend inside the selected workspace; OpenCode code tools are overridden so file, shell, search, and patch actions execute remotely over SSH where the code lives.
## Development ## Development
+3 -2
View File
@@ -3,5 +3,6 @@
1. Build a local Electron shell that reads SSH targets, starts SSH tunnels, and opens the remote Theia frontend. 1. Build a local Electron shell that reads SSH targets, starts SSH tunnels, and opens the remote Theia frontend.
2. Install a versioned Theia server bundle into `~/.git.zone/ide-server/<version>` on remote SSH hosts. 2. Install a versioned Theia server bundle into `~/.git.zone/ide-server/<version>` on remote SSH hosts.
3. Run the Theia backend and OpenCode server on the remote host, bound to `127.0.0.1`. 3. Run the Theia backend and OpenCode server on the remote host, bound to `127.0.0.1`.
4. Expose OpenCode through a Theia backend service, not directly to the Electron renderer. 4. Run OpenCode in the native Electron shell with tool overrides that execute against the selected remote project over SSH.
5. Render OpenCode sessions, messages, permissions, diffs, todos, and Git.Zone commands inside Theia. 5. Render OpenCode sessions, messages, permissions, diffs, todos, and Git.Zone commands in the native shell UI.
6. Keep Theia integration scoped to editor context and future intellisense data for OpenCode tools.
+24 -1
View File
@@ -1,5 +1,6 @@
import { tap, expect } from '@git.zone/tstest/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import { parseServerSentEvent } from '../packages/opencode-bridge/ts/index.js'; import * as fs from 'node:fs/promises';
import { parseServerSentEvent, sanitizeOpenCodeEventForRenderer } from '../packages/opencode-bridge/ts/index.js';
tap.test('should parse named opencode sse events', async () => { tap.test('should parse named opencode sse events', async () => {
const event = parseServerSentEvent('id: 1\nevent: server.connected\ndata: {"type":"server.connected"}\n'); const event = parseServerSentEvent('id: 1\nevent: server.connected\ndata: {"type":"server.connected"}\n');
@@ -14,4 +15,26 @@ tap.test('should infer opencode event type from json data', async () => {
expect(event!.data).toEqual({ type: 'session.updated', properties: { id: 'abc' } }); expect(event!.data).toEqual({ type: 'session.updated', properties: { id: 'abc' } });
}); });
tap.test('should sanitize opencode events for renderer delivery', async () => {
const event = parseServerSentEvent('id: 2\nretry: 1000\nevent: permission.asked\ndata: {"permissionID":"perm-1"}\n')!;
const sanitized = sanitizeOpenCodeEventForRenderer(event);
expect(sanitized).toEqual({
type: 'permission.asked',
id: '2',
retry: 1000,
data: { permissionID: 'perm-1' },
});
expect(Object.prototype.hasOwnProperty.call(sanitized, 'raw')).toEqual(false);
});
tap.test('should keep electron shell opencode resolution IDE-local', async () => {
const source = await fs.readFile(new URL('../applications/electron-shell/ts/main.ts', import.meta.url), 'utf8');
expect(source.includes('process.env.OPENCODE_BINARY')).toEqual(false);
expect(source.includes("'.opencode', 'bin', 'opencode'")).toEqual(false);
expect(source.includes('/usr/local/bin/opencode')).toEqual(false);
expect(source.includes('/usr/bin/opencode')).toEqual(false);
});
export default tap.start(); export default tap.start();
@@ -1,22 +0,0 @@
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command.js';
import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry.js';
import { MessageService } from '@theia/core/lib/common/message-service.js';
import { ContainerModule } from '@theia/core/shared/inversify/index.js';
import { type IGitZoneOpenCodeServer } from '../common/gitzone-opencode-protocol.js';
export declare const GitZoneOpenCodeHealthCommand: {
id: string;
label: string;
};
export declare const GitZoneOpenCodeNewSessionCommand: {
id: string;
label: string;
};
export declare class GitZoneOpenCodeContribution implements CommandContribution, MenuContribution {
protected readonly openCodeServer: IGitZoneOpenCodeServer;
protected readonly messages: MessageService;
registerCommands(registry: CommandRegistry): void;
registerMenus(menus: MenuModelRegistry): void;
}
declare const _default: ContainerModule;
export default _default;
//# sourceMappingURL=gitzone-opencode-frontend-module.d.ts.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-frontend-module.d.ts","sourceRoot":"","sources":["../../src/browser/gitzone-opencode-frontend-module.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,oDAAoD,CAAC;AACzG,OAAO,EAAE,cAAc,EAAE,MAAM,2CAA2C,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAsB,MAAM,uCAAuC,CAAC;AAC5F,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,wCAAwC,CAAC;AAEhD,eAAO,MAAM,4BAA4B;;;CAGxC,CAAC;AAEF,eAAO,MAAM,gCAAgC;;;CAG5C,CAAC;AAEF,qBACa,2BAA4B,YAAW,mBAAmB,EAAE,gBAAgB;IAEvF,SAAS,CAAC,QAAQ,CAAC,cAAc,EAAG,sBAAsB,CAAC;IAG3D,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAG,cAAc,CAAC;IAE7C,gBAAgB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAejD,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;CAU9C;;AAQD,wBAWG"}
@@ -1,83 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitZoneOpenCodeContribution = exports.GitZoneOpenCodeNewSessionCommand = exports.GitZoneOpenCodeHealthCommand = void 0;
const common_menus_js_1 = require("@theia/core/lib/browser/common-menus.js");
const ws_connection_provider_js_1 = require("@theia/core/lib/browser/messaging/ws-connection-provider.js");
const command_js_1 = require("@theia/core/lib/common/command.js");
const menu_model_registry_js_1 = require("@theia/core/lib/common/menu/menu-model-registry.js");
const message_service_js_1 = require("@theia/core/lib/common/message-service.js");
const index_js_1 = require("@theia/core/shared/inversify/index.js");
const gitzone_opencode_protocol_js_1 = require("../common/gitzone-opencode-protocol.js");
exports.GitZoneOpenCodeHealthCommand = {
id: 'gitzone.opencode.health',
label: 'OpenCode: Check Health',
};
exports.GitZoneOpenCodeNewSessionCommand = {
id: 'gitzone.opencode.newSession',
label: 'OpenCode: New Session',
};
let GitZoneOpenCodeContribution = class GitZoneOpenCodeContribution {
openCodeServer;
messages;
registerCommands(registry) {
registry.registerCommand(exports.GitZoneOpenCodeHealthCommand, {
execute: async () => {
const health = await this.openCodeServer.health();
await this.messages.info(`OpenCode health: ${JSON.stringify(health)}`);
},
});
registry.registerCommand(exports.GitZoneOpenCodeNewSessionCommand, {
execute: async () => {
const session = await this.openCodeServer.createSession('Git.Zone IDE Session');
await this.messages.info(`OpenCode session created: ${JSON.stringify(session)}`);
},
});
}
registerMenus(menus) {
menus.registerMenuAction(common_menus_js_1.CommonMenus.VIEW_VIEWS, {
commandId: exports.GitZoneOpenCodeHealthCommand.id,
label: exports.GitZoneOpenCodeHealthCommand.label,
});
menus.registerMenuAction(common_menus_js_1.CommonMenus.VIEW_VIEWS, {
commandId: exports.GitZoneOpenCodeNewSessionCommand.id,
label: exports.GitZoneOpenCodeNewSessionCommand.label,
});
}
};
exports.GitZoneOpenCodeContribution = GitZoneOpenCodeContribution;
__decorate([
(0, index_js_1.inject)(gitzone_opencode_protocol_js_1.GitZoneOpenCodeServer),
__metadata("design:type", Object)
], GitZoneOpenCodeContribution.prototype, "openCodeServer", void 0);
__decorate([
(0, index_js_1.inject)(message_service_js_1.MessageService),
__metadata("design:type", message_service_js_1.MessageService)
], GitZoneOpenCodeContribution.prototype, "messages", void 0);
exports.GitZoneOpenCodeContribution = GitZoneOpenCodeContribution = __decorate([
(0, index_js_1.injectable)()
], GitZoneOpenCodeContribution);
const openCodeClient = {
onOpenCodeEvent: (event) => {
globalThis.dispatchEvent(new CustomEvent('gitzone-opencode-event', { detail: event }));
},
};
exports.default = new index_js_1.ContainerModule((bind) => {
bind(gitzone_opencode_protocol_js_1.GitZoneOpenCodeServer)
.toDynamicValue((context) => context.container
.get(ws_connection_provider_js_1.WebSocketConnectionProvider)
.createProxy(gitzone_opencode_protocol_js_1.gitZoneOpenCodePath, openCodeClient))
.inSingletonScope();
bind(GitZoneOpenCodeContribution).toSelf().inSingletonScope();
bind(command_js_1.CommandContribution).toService(GitZoneOpenCodeContribution);
bind(menu_model_registry_js_1.MenuContribution).toService(GitZoneOpenCodeContribution);
});
//# sourceMappingURL=gitzone-opencode-frontend-module.js.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-frontend-module.js","sourceRoot":"","sources":["../../src/browser/gitzone-opencode-frontend-module.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,6EAAsE;AACtE,2GAA0G;AAC1G,kEAAyF;AACzF,+FAAyG;AACzG,kFAA2E;AAC3E,oEAA4F;AAC5F,yFAKgD;AAEnC,QAAA,4BAA4B,GAAG;IAC1C,EAAE,EAAE,yBAAyB;IAC7B,KAAK,EAAE,wBAAwB;CAChC,CAAC;AAEW,QAAA,gCAAgC,GAAG;IAC9C,EAAE,EAAE,6BAA6B;IACjC,KAAK,EAAE,uBAAuB;CAC/B,CAAC;AAGK,IAAM,2BAA2B,GAAjC,MAAM,2BAA2B;IAEnB,cAAc,CAA0B;IAGxC,QAAQ,CAAkB;IAE7C,gBAAgB,CAAC,QAAyB;QACxC,QAAQ,CAAC,eAAe,CAAC,oCAA4B,EAAE;YACrD,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;gBAClD,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;SACF,CAAC,CAAC;QACH,QAAQ,CAAC,eAAe,CAAC,wCAAgC,EAAE;YACzD,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;gBAChF,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,6BAA6B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACnF,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IAED,aAAa,CAAC,KAAwB;QACpC,KAAK,CAAC,kBAAkB,CAAC,6BAAW,CAAC,UAAU,EAAE;YAC/C,SAAS,EAAE,oCAA4B,CAAC,EAAE;YAC1C,KAAK,EAAE,oCAA4B,CAAC,KAAK;SAC1C,CAAC,CAAC;QACH,KAAK,CAAC,kBAAkB,CAAC,6BAAW,CAAC,UAAU,EAAE;YAC/C,SAAS,EAAE,wCAAgC,CAAC,EAAE;YAC9C,KAAK,EAAE,wCAAgC,CAAC,KAAK;SAC9C,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AAhCY,kEAA2B;AAEnB;IADlB,IAAA,iBAAM,EAAC,oDAAqB,CAAC;;mEAC6B;AAGxC;IADlB,IAAA,iBAAM,EAAC,mCAAc,CAAC;8BACO,mCAAc;6DAAC;sCALlC,2BAA2B;IADvC,IAAA,qBAAU,GAAE;GACA,2BAA2B,CAgCvC;AAED,MAAM,cAAc,GAA2B;IAC7C,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE;QACzB,UAAU,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,wBAAwB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACzF,CAAC;CACF,CAAC;AAEF,kBAAe,IAAI,0BAAe,CAAC,CAAC,IAAI,EAAE,EAAE;IAC1C,IAAI,CAAC,oDAAqB,CAAC;SACxB,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE,CAC1B,OAAO,CAAC,SAAS;SACd,GAAG,CAAC,uDAA2B,CAAC;SAChC,WAAW,CAAyB,kDAAmB,EAAE,cAAc,CAAC,CAC5E;SACA,gBAAgB,EAAE,CAAC;IACtB,IAAI,CAAC,2BAA2B,CAAC,CAAC,MAAM,EAAE,CAAC,gBAAgB,EAAE,CAAC;IAC9D,IAAI,CAAC,gCAAmB,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;IACjE,IAAI,CAAC,yCAAgB,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;AAChE,CAAC,CAAC,CAAC"}
@@ -1,51 +0,0 @@
export declare const gitZoneOpenCodePath = "/services/git-zone/opencode";
export declare const GitZoneOpenCodeServer: unique symbol;
export interface IGitZoneOpenCodeConnectionInfo {
baseUrl: string;
port: number;
workspacePath: string;
autoStart: boolean;
}
export interface IGitZoneOpenCodeEvent {
type: string;
id?: string;
retry?: number;
data?: unknown;
raw: string;
}
export interface IGitZoneOpenCodeClient {
onOpenCodeEvent(event: IGitZoneOpenCodeEvent): void;
}
export interface IGitZoneOpenCodePromptBody {
messageID?: string;
model?: {
providerID: string;
modelID: string;
};
agent?: string;
noReply?: boolean;
system?: string;
tools?: Record<string, boolean>;
parts: Array<{
type: string;
[key: string]: unknown;
}>;
}
export interface IGitZoneOpenCodeServer {
setClient(client: IGitZoneOpenCodeClient | undefined): void;
getConnectionInfo(): Promise<IGitZoneOpenCodeConnectionInfo>;
health(): Promise<unknown>;
providers(): Promise<unknown>;
agents(): Promise<unknown>;
sessions(): Promise<unknown>;
createSession(title?: string): Promise<unknown>;
messages(sessionId: string, limit?: number): Promise<unknown>;
prompt(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<unknown>;
promptAsync(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<void>;
command(sessionId: string, command: string, commandArguments?: string): Promise<unknown>;
abort(sessionId: string): Promise<unknown>;
diff(sessionId: string, messageId?: string): Promise<unknown>;
todo(sessionId: string): Promise<unknown>;
respondToPermission(sessionId: string, permissionId: string, response: string, remember?: boolean): Promise<unknown>;
}
//# sourceMappingURL=gitzone-opencode-protocol.d.ts.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-protocol.d.ts","sourceRoot":"","sources":["../../src/common/gitzone-opencode-protocol.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAEjE,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,eAAe,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,SAAS,GAAG,IAAI,CAAC;IAC5D,iBAAiB,IAAI,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAC7D,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3B,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3B,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzF,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtH"}
@@ -1,6 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitZoneOpenCodeServer = exports.gitZoneOpenCodePath = void 0;
exports.gitZoneOpenCodePath = '/services/git-zone/opencode';
exports.GitZoneOpenCodeServer = Symbol('GitZoneOpenCodeServer');
//# sourceMappingURL=gitzone-opencode-protocol.js.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-protocol.js","sourceRoot":"","sources":["../../src/common/gitzone-opencode-protocol.ts"],"names":[],"mappings":";;;AAAa,QAAA,mBAAmB,GAAG,6BAA6B,CAAC;AAEpD,QAAA,qBAAqB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC"}
@@ -1,4 +0,0 @@
import { ContainerModule } from '@theia/core/shared/inversify/index.js';
declare const _default: ContainerModule;
export default _default;
//# sourceMappingURL=gitzone-opencode-backend-module.d.ts.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-backend-module.d.ts","sourceRoot":"","sources":["../../src/node/gitzone-opencode-backend-module.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;;AASxE,wBAaG"}
@@ -1,21 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const backend_application_js_1 = require("@theia/core/lib/node/backend-application.js");
const handler_js_1 = require("@theia/core/lib/common/messaging/handler.js");
const proxy_factory_js_1 = require("@theia/core/lib/common/messaging/proxy-factory.js");
const index_js_1 = require("@theia/core/shared/inversify/index.js");
const gitzone_opencode_protocol_js_1 = require("../common/gitzone-opencode-protocol.js");
const gitzone_opencode_node_service_js_1 = require("./gitzone-opencode-node-service.js");
exports.default = new index_js_1.ContainerModule((bind) => {
bind(gitzone_opencode_node_service_js_1.GitZoneOpenCodeNodeService).toSelf().inSingletonScope();
bind(gitzone_opencode_protocol_js_1.GitZoneOpenCodeServer).toService(gitzone_opencode_node_service_js_1.GitZoneOpenCodeNodeService);
bind(backend_application_js_1.BackendApplicationContribution).toService(gitzone_opencode_node_service_js_1.GitZoneOpenCodeNodeService);
bind(handler_js_1.ConnectionHandler)
.toDynamicValue((context) => new proxy_factory_js_1.RpcConnectionHandler(gitzone_opencode_protocol_js_1.gitZoneOpenCodePath, (client) => {
const server = context.container.get(gitzone_opencode_protocol_js_1.GitZoneOpenCodeServer);
server.setClient(client);
return server;
}))
.inSingletonScope();
});
//# sourceMappingURL=gitzone-opencode-backend-module.js.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-backend-module.js","sourceRoot":"","sources":["../../src/node/gitzone-opencode-backend-module.ts"],"names":[],"mappings":";;AAAA,wFAA6F;AAC7F,4EAAgF;AAChF,wFAAyF;AACzF,oEAAwE;AACxE,yFAKgD;AAChD,yFAAgF;AAEhF,kBAAe,IAAI,0BAAe,CAAC,CAAC,IAAI,EAAE,EAAE;IAC1C,IAAI,CAAC,6DAA0B,CAAC,CAAC,MAAM,EAAE,CAAC,gBAAgB,EAAE,CAAC;IAC7D,IAAI,CAAC,oDAAqB,CAAC,CAAC,SAAS,CAAC,6DAA0B,CAAC,CAAC;IAClE,IAAI,CAAC,uDAA8B,CAAC,CAAC,SAAS,CAAC,6DAA0B,CAAC,CAAC;IAC3E,IAAI,CAAC,8BAAiB,CAAC;SACpB,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE,CAC1B,IAAI,uCAAoB,CAAyB,kDAAmB,EAAE,CAAC,MAAM,EAAE,EAAE;QAC/E,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,GAAG,CAAyB,oDAAqB,CAAC,CAAC;QACpF,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CACH;SACA,gBAAgB,EAAE,CAAC;AACxB,CAAC,CAAC,CAAC"}
@@ -1,36 +0,0 @@
import { OpenCodeServerClient } from '@git.zone/ide-opencode-bridge';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application.js';
import type { IGitZoneOpenCodeClient, IGitZoneOpenCodeConnectionInfo, IGitZoneOpenCodePromptBody, IGitZoneOpenCodeServer } from '../common/gitzone-opencode-protocol.js';
import * as plugins from './plugins.js';
export declare class GitZoneOpenCodeNodeService implements IGitZoneOpenCodeServer, BackendApplicationContribution {
protected client: IGitZoneOpenCodeClient | undefined;
protected eventAbortController: AbortController | undefined;
protected openCodeProcess: plugins.childProcess.ChildProcess | undefined;
initialize(): void;
onStop(): void;
setClient(client: IGitZoneOpenCodeClient | undefined): void;
getConnectionInfo(): Promise<IGitZoneOpenCodeConnectionInfo>;
health(): Promise<unknown>;
providers(): Promise<unknown>;
agents(): Promise<unknown>;
sessions(): Promise<unknown>;
createSession(title?: string): Promise<unknown>;
messages(sessionId: string, limit?: number): Promise<unknown>;
prompt(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<unknown>;
promptAsync(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<void>;
command(sessionId: string, command: string, commandArguments?: string): Promise<unknown>;
abort(sessionId: string): Promise<unknown>;
diff(sessionId: string, messageId?: string): Promise<unknown>;
todo(sessionId: string): Promise<unknown>;
respondToPermission(sessionId: string, permissionId: string, response: string, remember?: boolean): Promise<unknown>;
protected ensureOpenCodeStarted(): Promise<void>;
protected restartEventStream(): void;
protected createClient(): OpenCodeServerClient;
protected get workspacePath(): string;
protected get port(): number;
protected get baseUrl(): string;
protected get username(): string;
protected get password(): string;
protected get autoStart(): boolean;
}
//# sourceMappingURL=gitzone-opencode-node-service.d.ts.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-node-service.d.ts","sourceRoot":"","sources":["../../src/node/gitzone-opencode-node-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,8BAA8B,EAAE,MAAM,6CAA6C,CAAC;AAE7F,OAAO,KAAK,EACV,sBAAsB,EACtB,8BAA8B,EAC9B,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,wCAAwC,CAAC;AAChD,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AAExC,qBACa,0BAA2B,YAAW,sBAAsB,EAAE,8BAA8B;IACvG,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,SAAS,CAAC;IACrD,SAAS,CAAC,oBAAoB,EAAE,eAAe,GAAG,SAAS,CAAC;IAC5D,SAAS,CAAC,eAAe,EAAE,OAAO,CAAC,YAAY,CAAC,YAAY,GAAG,SAAS,CAAC;IAEzE,UAAU,IAAI,IAAI;IAMlB,MAAM,IAAI,IAAI;IAOd,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,SAAS,GAAG,IAAI;IAKrD,iBAAiB,IAAI,OAAO,CAAC,8BAA8B,CAAC;IAS5D,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAK1B,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAI7B,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAI1B,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAI5B,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI/C,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7D,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7E,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/E,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,SAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IAIpF,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI1C,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7D,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIzC,mBAAmB,CACvB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,OAAO,GACjB,OAAO,CAAC,OAAO,CAAC;cAIH,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCtD,SAAS,CAAC,kBAAkB,IAAI,IAAI;IAmBpC,SAAS,CAAC,YAAY;IAQtB,SAAS,KAAK,aAAa,WAE1B;IAED,SAAS,KAAK,IAAI,WAGjB;IAED,SAAS,KAAK,OAAO,WAEpB;IAED,SAAS,KAAK,QAAQ,WAErB;IAED,SAAS,KAAK,QAAQ,WAErB;IAED,SAAS,KAAK,SAAS,YAEtB;CACF"}
@@ -1,182 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitZoneOpenCodeNodeService = void 0;
const ide_opencode_bridge_1 = require("@git.zone/ide-opencode-bridge");
const index_js_1 = require("@theia/core/shared/inversify/index.js");
const plugins = __importStar(require("./plugins.js"));
let GitZoneOpenCodeNodeService = class GitZoneOpenCodeNodeService {
client;
eventAbortController;
openCodeProcess;
initialize() {
if (this.autoStart) {
void this.ensureOpenCodeStarted();
}
}
onStop() {
this.eventAbortController?.abort();
if (this.openCodeProcess && this.openCodeProcess.exitCode === null) {
this.openCodeProcess.kill('SIGTERM');
}
}
setClient(client) {
this.client = client;
this.restartEventStream();
}
async getConnectionInfo() {
return {
baseUrl: this.baseUrl,
port: this.port,
workspacePath: this.workspacePath,
autoStart: this.autoStart,
};
}
async health() {
await this.ensureOpenCodeStarted();
return this.createClient().health();
}
async providers() {
return this.createClient().providers();
}
async agents() {
return this.createClient().agents();
}
async sessions() {
return this.createClient().sessions();
}
async createSession(title) {
return this.createClient().createSession(title ? { title } : {});
}
async messages(sessionId, limit) {
return this.createClient().messages(sessionId, limit);
}
async prompt(sessionId, body) {
return this.createClient().prompt(sessionId, body);
}
async promptAsync(sessionId, body) {
await this.createClient().promptAsync(sessionId, body);
}
async command(sessionId, command, commandArguments = '') {
return this.createClient().command(sessionId, { command, arguments: commandArguments });
}
async abort(sessionId) {
return this.createClient().abort(sessionId);
}
async diff(sessionId, messageId) {
return this.createClient().diff(sessionId, messageId);
}
async todo(sessionId) {
return this.createClient().todo(sessionId);
}
async respondToPermission(sessionId, permissionId, response, remember) {
return this.createClient().respondToPermission(sessionId, permissionId, { response, remember });
}
async ensureOpenCodeStarted() {
try {
await this.createClient().health();
return;
}
catch {
if (!this.autoStart || this.openCodeProcess) {
return;
}
}
this.openCodeProcess = plugins.childProcess.spawn('opencode', ['serve', '--hostname', '127.0.0.1', '--port', `${this.port}`], {
cwd: this.workspacePath,
env: {
...process.env,
OPENCODE_SERVER_USERNAME: this.username,
OPENCODE_SERVER_PASSWORD: this.password,
},
shell: false,
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: true,
});
this.openCodeProcess.once('error', (error) => {
console.warn(`OpenCode server autostart failed: ${error.message}`);
this.openCodeProcess = undefined;
});
this.openCodeProcess.once('exit', () => {
this.openCodeProcess = undefined;
});
}
restartEventStream() {
this.eventAbortController?.abort();
if (!this.client) {
return;
}
const abortController = new AbortController();
this.eventAbortController = abortController;
void (async () => {
try {
await this.ensureOpenCodeStarted();
for await (const event of this.createClient().events(abortController.signal)) {
this.client?.onOpenCodeEvent(event);
}
}
catch {
// The UI can explicitly call health() for detailed connection diagnostics.
}
})();
}
createClient() {
return new ide_opencode_bridge_1.OpenCodeServerClient({
baseUrl: this.baseUrl,
username: this.username,
password: this.password,
});
}
get workspacePath() {
return process.env.GITZONE_IDE_WORKSPACE || process.cwd();
}
get port() {
const port = Number(process.env.GITZONE_IDE_OPENCODE_PORT || '4096');
return Number.isInteger(port) && port > 0 ? port : 4096;
}
get baseUrl() {
return `http://127.0.0.1:${this.port}`;
}
get username() {
return process.env.OPENCODE_SERVER_USERNAME || 'opencode';
}
get password() {
return process.env.OPENCODE_SERVER_PASSWORD || '';
}
get autoStart() {
return process.env.GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART !== '1';
}
};
exports.GitZoneOpenCodeNodeService = GitZoneOpenCodeNodeService;
exports.GitZoneOpenCodeNodeService = GitZoneOpenCodeNodeService = __decorate([
(0, index_js_1.injectable)()
], GitZoneOpenCodeNodeService);
//# sourceMappingURL=gitzone-opencode-node-service.js.map
@@ -1 +0,0 @@
{"version":3,"file":"gitzone-opencode-node-service.js","sourceRoot":"","sources":["../../src/node/gitzone-opencode-node-service.ts"],"names":[],"mappings":"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"}
@@ -1,3 +0,0 @@
import * as childProcess from 'node:child_process';
export { childProcess };
//# sourceMappingURL=plugins.d.ts.map
@@ -1 +0,0 @@
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/node/plugins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -1,29 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.childProcess = void 0;
const childProcess = __importStar(require("node:child_process"));
exports.childProcess = childProcess;
//# sourceMappingURL=plugins.js.map
@@ -1 +0,0 @@
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../../src/node/plugins.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iEAAmD;AAE1C,oCAAY"}
@@ -1,20 +0,0 @@
{
"name": "@git.zone/ide-extension-opencode",
"version": "0.1.0",
"private": true,
"keywords": ["theia-extension"],
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@git.zone/ide-opencode-bridge": "workspace:*",
"@theia/core": "1.71.0"
},
"theiaExtensions": [
{
"frontend": "lib/browser/gitzone-opencode-frontend-module",
"backend": "lib/node/gitzone-opencode-backend-module"
}
],
"files": ["src", "lib"]
}
@@ -1,76 +0,0 @@
import { CommonMenus } from '@theia/core/lib/browser/common-menus.js';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider.js';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command.js';
import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry.js';
import { MessageService } from '@theia/core/lib/common/message-service.js';
import { ContainerModule, inject, injectable } from '@theia/core/shared/inversify/index.js';
import {
GitZoneOpenCodeServer,
gitZoneOpenCodePath,
type IGitZoneOpenCodeClient,
type IGitZoneOpenCodeServer,
} from '../common/gitzone-opencode-protocol.js';
export const GitZoneOpenCodeHealthCommand = {
id: 'gitzone.opencode.health',
label: 'OpenCode: Check Health',
};
export const GitZoneOpenCodeNewSessionCommand = {
id: 'gitzone.opencode.newSession',
label: 'OpenCode: New Session',
};
@injectable()
export class GitZoneOpenCodeContribution implements CommandContribution, MenuContribution {
@inject(GitZoneOpenCodeServer)
protected readonly openCodeServer!: IGitZoneOpenCodeServer;
@inject(MessageService)
protected readonly messages!: MessageService;
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(GitZoneOpenCodeHealthCommand, {
execute: async () => {
const health = await this.openCodeServer.health();
await this.messages.info(`OpenCode health: ${JSON.stringify(health)}`);
},
});
registry.registerCommand(GitZoneOpenCodeNewSessionCommand, {
execute: async () => {
const session = await this.openCodeServer.createSession('Git.Zone IDE Session');
await this.messages.info(`OpenCode session created: ${JSON.stringify(session)}`);
},
});
}
registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
commandId: GitZoneOpenCodeHealthCommand.id,
label: GitZoneOpenCodeHealthCommand.label,
});
menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
commandId: GitZoneOpenCodeNewSessionCommand.id,
label: GitZoneOpenCodeNewSessionCommand.label,
});
}
}
const openCodeClient: IGitZoneOpenCodeClient = {
onOpenCodeEvent: (event) => {
globalThis.dispatchEvent(new CustomEvent('gitzone-opencode-event', { detail: event }));
},
};
export default new ContainerModule((bind) => {
bind(GitZoneOpenCodeServer)
.toDynamicValue((context) =>
context.container
.get(WebSocketConnectionProvider)
.createProxy<IGitZoneOpenCodeServer>(gitZoneOpenCodePath, openCodeClient),
)
.inSingletonScope();
bind(GitZoneOpenCodeContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(GitZoneOpenCodeContribution);
bind(MenuContribution).toService(GitZoneOpenCodeContribution);
});
@@ -1,53 +0,0 @@
export const gitZoneOpenCodePath = '/services/git-zone/opencode';
export const GitZoneOpenCodeServer = Symbol('GitZoneOpenCodeServer');
export interface IGitZoneOpenCodeConnectionInfo {
baseUrl: string;
port: number;
workspacePath: string;
autoStart: boolean;
}
export interface IGitZoneOpenCodeEvent {
type: string;
id?: string;
retry?: number;
data?: unknown;
raw: string;
}
export interface IGitZoneOpenCodeClient {
onOpenCodeEvent(event: IGitZoneOpenCodeEvent): void;
}
export interface IGitZoneOpenCodePromptBody {
messageID?: string;
model?: {
providerID: string;
modelID: string;
};
agent?: string;
noReply?: boolean;
system?: string;
tools?: Record<string, boolean>;
parts: Array<{ type: string; [key: string]: unknown }>;
}
export interface IGitZoneOpenCodeServer {
setClient(client: IGitZoneOpenCodeClient | undefined): void;
getConnectionInfo(): Promise<IGitZoneOpenCodeConnectionInfo>;
health(): Promise<unknown>;
providers(): Promise<unknown>;
agents(): Promise<unknown>;
sessions(): Promise<unknown>;
createSession(title?: string): Promise<unknown>;
messages(sessionId: string, limit?: number): Promise<unknown>;
prompt(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<unknown>;
promptAsync(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<void>;
command(sessionId: string, command: string, commandArguments?: string): Promise<unknown>;
abort(sessionId: string): Promise<unknown>;
diff(sessionId: string, messageId?: string): Promise<unknown>;
todo(sessionId: string): Promise<unknown>;
respondToPermission(sessionId: string, permissionId: string, response: string, remember?: boolean): Promise<unknown>;
}
@@ -1,26 +0,0 @@
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application.js';
import { ConnectionHandler } from '@theia/core/lib/common/messaging/handler.js';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory.js';
import { ContainerModule } from '@theia/core/shared/inversify/index.js';
import {
GitZoneOpenCodeServer,
gitZoneOpenCodePath,
type IGitZoneOpenCodeClient,
type IGitZoneOpenCodeServer,
} from '../common/gitzone-opencode-protocol.js';
import { GitZoneOpenCodeNodeService } from './gitzone-opencode-node-service.js';
export default new ContainerModule((bind) => {
bind(GitZoneOpenCodeNodeService).toSelf().inSingletonScope();
bind(GitZoneOpenCodeServer).toService(GitZoneOpenCodeNodeService);
bind(BackendApplicationContribution).toService(GitZoneOpenCodeNodeService);
bind(ConnectionHandler)
.toDynamicValue((context) =>
new RpcConnectionHandler<IGitZoneOpenCodeClient>(gitZoneOpenCodePath, (client) => {
const server = context.container.get<IGitZoneOpenCodeServer>(GitZoneOpenCodeServer);
server.setClient(client);
return server;
}),
)
.inSingletonScope();
});
@@ -1,188 +0,0 @@
import { OpenCodeServerClient } from '@git.zone/ide-opencode-bridge';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application.js';
import { injectable } from '@theia/core/shared/inversify/index.js';
import type {
IGitZoneOpenCodeClient,
IGitZoneOpenCodeConnectionInfo,
IGitZoneOpenCodePromptBody,
IGitZoneOpenCodeServer,
} from '../common/gitzone-opencode-protocol.js';
import * as plugins from './plugins.js';
@injectable()
export class GitZoneOpenCodeNodeService implements IGitZoneOpenCodeServer, BackendApplicationContribution {
protected client: IGitZoneOpenCodeClient | undefined;
protected eventAbortController: AbortController | undefined;
protected openCodeProcess: plugins.childProcess.ChildProcess | undefined;
initialize(): void {
if (this.autoStart) {
void this.ensureOpenCodeStarted();
}
}
onStop(): void {
this.eventAbortController?.abort();
if (this.openCodeProcess && this.openCodeProcess.exitCode === null) {
this.openCodeProcess.kill('SIGTERM');
}
}
setClient(client: IGitZoneOpenCodeClient | undefined): void {
this.client = client;
this.restartEventStream();
}
async getConnectionInfo(): Promise<IGitZoneOpenCodeConnectionInfo> {
return {
baseUrl: this.baseUrl,
port: this.port,
workspacePath: this.workspacePath,
autoStart: this.autoStart,
};
}
async health(): Promise<unknown> {
await this.ensureOpenCodeStarted();
return this.createClient().health();
}
async providers(): Promise<unknown> {
return this.createClient().providers();
}
async agents(): Promise<unknown> {
return this.createClient().agents();
}
async sessions(): Promise<unknown> {
return this.createClient().sessions();
}
async createSession(title?: string): Promise<unknown> {
return this.createClient().createSession(title ? { title } : {});
}
async messages(sessionId: string, limit?: number): Promise<unknown> {
return this.createClient().messages(sessionId, limit);
}
async prompt(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<unknown> {
return this.createClient().prompt(sessionId, body);
}
async promptAsync(sessionId: string, body: IGitZoneOpenCodePromptBody): Promise<void> {
await this.createClient().promptAsync(sessionId, body);
}
async command(sessionId: string, command: string, commandArguments = ''): Promise<unknown> {
return this.createClient().command(sessionId, { command, arguments: commandArguments });
}
async abort(sessionId: string): Promise<unknown> {
return this.createClient().abort(sessionId);
}
async diff(sessionId: string, messageId?: string): Promise<unknown> {
return this.createClient().diff(sessionId, messageId);
}
async todo(sessionId: string): Promise<unknown> {
return this.createClient().todo(sessionId);
}
async respondToPermission(
sessionId: string,
permissionId: string,
response: string,
remember?: boolean,
): Promise<unknown> {
return this.createClient().respondToPermission(sessionId, permissionId, { response, remember });
}
protected async ensureOpenCodeStarted(): Promise<void> {
try {
await this.createClient().health();
return;
} catch {
if (!this.autoStart || this.openCodeProcess) {
return;
}
}
this.openCodeProcess = plugins.childProcess.spawn(
'opencode',
['serve', '--hostname', '127.0.0.1', '--port', `${this.port}`],
{
cwd: this.workspacePath,
env: {
...process.env,
OPENCODE_SERVER_USERNAME: this.username,
OPENCODE_SERVER_PASSWORD: this.password,
},
shell: false,
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: true,
},
);
this.openCodeProcess.once('error', (error) => {
console.warn(`OpenCode server autostart failed: ${error.message}`);
this.openCodeProcess = undefined;
});
this.openCodeProcess.once('exit', () => {
this.openCodeProcess = undefined;
});
}
protected restartEventStream(): void {
this.eventAbortController?.abort();
if (!this.client) {
return;
}
const abortController = new AbortController();
this.eventAbortController = abortController;
void (async () => {
try {
await this.ensureOpenCodeStarted();
for await (const event of this.createClient().events(abortController.signal)) {
this.client?.onOpenCodeEvent(event);
}
} catch {
// The UI can explicitly call health() for detailed connection diagnostics.
}
})();
}
protected createClient() {
return new OpenCodeServerClient({
baseUrl: this.baseUrl,
username: this.username,
password: this.password,
});
}
protected get workspacePath() {
return process.env.GITZONE_IDE_WORKSPACE || process.cwd();
}
protected get port() {
const port = Number(process.env.GITZONE_IDE_OPENCODE_PORT || '4096');
return Number.isInteger(port) && port > 0 ? port : 4096;
}
protected get baseUrl() {
return `http://127.0.0.1:${this.port}`;
}
protected get username() {
return process.env.OPENCODE_SERVER_USERNAME || 'opencode';
}
protected get password() {
return process.env.OPENCODE_SERVER_PASSWORD || '';
}
protected get autoStart() {
return process.env.GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART !== '1';
}
}
@@ -1,3 +0,0 @@
import * as childProcess from 'node:child_process';
export { childProcess };
@@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"verbatimModuleSyntax": false,
"rootDir": "src",
"outDir": "lib",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}