|
|
|
@@ -213,6 +213,76 @@ class GitZoneIdeElectronShell {
|
|
|
|
|
progress(`Project ${project.title} is ready.`);
|
|
|
|
|
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() {
|
|
|
|
@@ -292,6 +362,7 @@ class GitZoneIdeElectronShell {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitForOpenCodeHealth(baseUrl, username, password, 15000);
|
|
|
|
|
startOpenCodeEventStream(runtime);
|
|
|
|
|
progress(`Local OpenCode server ready for ${project.title}; remote tools are bridged over SSH.`);
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
@@ -385,6 +456,30 @@ interface IOpenProjectInput {
|
|
|
|
|
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 {
|
|
|
|
|
id: string;
|
|
|
|
|
path: string;
|
|
|
|
@@ -425,6 +520,7 @@ interface ILocalOpenCodeRuntime {
|
|
|
|
|
process: childProcess.ChildProcess;
|
|
|
|
|
bridge: LocalOpenCodeToolBridge;
|
|
|
|
|
bridgeToken: string;
|
|
|
|
|
eventAbortController?: AbortController;
|
|
|
|
|
configDir: string;
|
|
|
|
|
proxyWorkspacePath: string;
|
|
|
|
|
baseUrl: string;
|
|
|
|
@@ -649,12 +745,13 @@ const writeOpenCodeBridgeConfig = async (configDir: string) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resolveOpenCodeExecutable = async () => {
|
|
|
|
|
const executableName = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
|
|
|
|
|
const candidates = [
|
|
|
|
|
process.env.GITZONE_IDE_OPENCODE_BINARY,
|
|
|
|
|
process.env.OPENCODE_BINARY,
|
|
|
|
|
path.join(os.homedir(), '.opencode', 'bin', 'opencode'),
|
|
|
|
|
'/usr/local/bin/opencode',
|
|
|
|
|
'/usr/bin/opencode',
|
|
|
|
|
path.join(electronPackageRoot, 'node_modules', '.bin', executableName),
|
|
|
|
|
path.join(workspaceRoot, 'node_modules', '.bin', executableName),
|
|
|
|
|
path.join(electronPackageRoot, 'node_modules', 'opencode-ai', 'bin', 'opencode'),
|
|
|
|
|
path.join(workspaceRoot, 'node_modules', 'opencode-ai', 'bin', 'opencode'),
|
|
|
|
|
].filter(Boolean) as string[];
|
|
|
|
|
for (const candidate of candidates) {
|
|
|
|
|
try {
|
|
|
|
@@ -662,7 +759,7 @@ const resolveOpenCodeExecutable = async () => {
|
|
|
|
|
return candidate;
|
|
|
|
|
} 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) => {
|
|
|
|
@@ -692,6 +789,7 @@ const waitForOpenCodeHealth = async (baseUrl: string, username: string, password
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const disposeLocalOpenCodeRuntime = (runtime: ILocalOpenCodeRuntime) => {
|
|
|
|
|
runtime.eventAbortController?.abort();
|
|
|
|
|
runtime.bridge.unregister(runtime.bridgeToken);
|
|
|
|
|
if (runtime.process.exitCode === null && !runtime.process.killed) {
|
|
|
|
|
runtime.process.kill('SIGTERM');
|
|
|
|
@@ -748,6 +846,68 @@ const normalizeOptionalPort = (value: number | undefined, label: string) => {
|
|
|
|
|
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) => ({
|
|
|
|
|
id: session.id,
|
|
|
|
|
hostAlias: session.target.hostAlias,
|
|
|
|
@@ -1085,7 +1245,7 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
<title>Git.Zone IDE</title>
|
|
|
|
|
<style>
|
|
|
|
|
* { 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); }
|
|
|
|
|
button, input, select { font: inherit; }
|
|
|
|
|
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; }
|
|
|
|
|
p { color: var(--muted); line-height: 1.45; }
|
|
|
|
|
[hidden] { display: none !important; }
|
|
|
|
|
.workbench { min-width: 980px; height: 100vh; display: grid; grid-template-rows: 30px 32px minmax(0, 1fr) 118px 22px; }
|
|
|
|
|
.titlebar { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 12px; background: #3c3c3c; color: #cccccc; user-select: none; }
|
|
|
|
|
.workbench { min-width: 980px; height: 100vh; display: grid; grid-template-rows: 30px 32px minmax(0, 1fr) 6px var(--panel-height) 22px; }
|
|
|
|
|
.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-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.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:hover { color: #ffffff; }
|
|
|
|
|
.content { min-height: 0; display: grid; grid-template-columns: 48px 300px minmax(0, 1fr); }
|
|
|
|
|
.activitybar { background: var(--activity); border-right: 1px solid #2b2b2b; display: flex; flex-direction: column; align-items: stretch; justify-content: space-between; padding: 4px 0; }
|
|
|
|
|
.content { grid-row: 3; min-height: 0; display: grid; grid-template-columns: 48px var(--sidebar-width) 6px minmax(0, 1fr); overflow: hidden; }
|
|
|
|
|
.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; }
|
|
|
|
|
.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); }
|
|
|
|
@@ -1117,7 +1277,7 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
.activity-button svg { width: 24px; height: 24px; }
|
|
|
|
|
.activity-button svg [fill] { fill: currentColor; }
|
|
|
|
|
.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-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; }
|
|
|
|
@@ -1130,6 +1290,22 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
.sidebar-form .actions button { flex: 1; }
|
|
|
|
|
.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; }
|
|
|
|
|
.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-list, .project-list { overflow: auto; padding-bottom: 8px; }
|
|
|
|
|
.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; }
|
|
|
|
|
.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; }
|
|
|
|
|
.main { min-width: 0; min-height: 0; background: var(--editor); display: grid; grid-template-rows: 35px minmax(0, 1fr); }
|
|
|
|
|
.main-header { display: flex; align-items: center; padding: 0 14px; background: #1f1f1f; border-bottom: 1px solid var(--border); color: #ffffff; }
|
|
|
|
|
.view { min-width: 0; min-height: 0; overflow: auto; padding: 28px 34px 34px; }
|
|
|
|
|
.splitter { position: relative; background: #252526; }
|
|
|
|
|
.splitter::before { content: ''; position: absolute; background: transparent; }
|
|
|
|
|
.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; }
|
|
|
|
|
webview { width: 100%; height: 100%; border: 0; background: #111111; }
|
|
|
|
|
.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-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; }
|
|
|
|
|
.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; }
|
|
|
|
|
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; }
|
|
|
|
|
@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 { 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; }
|
|
|
|
|
.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>
|
|
|
|
|
</head>
|
|
|
|
|
<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 id="projectTabs" class="project-tabs"></div>
|
|
|
|
|
<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>
|
|
|
|
|
<div class="sidebar-toolbar">
|
|
|
|
|
<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>
|
|
|
|
|
</section>
|
|
|
|
|
<section id="outputPanel" class="sidebar-panel" data-sidebar-panel="output" hidden>
|
|
|
|
@@ -1260,8 +1474,14 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</aside>
|
|
|
|
|
<div id="sidebarResizer" class="splitter sidebar-resizer" role="separator" aria-label="Resize sidebar" aria-orientation="vertical"></div>
|
|
|
|
|
<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 class="welcome">
|
|
|
|
|
<h1>Connect to Remote Host</h1>
|
|
|
|
@@ -1300,29 +1520,42 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</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-title">Output</div>
|
|
|
|
|
<pre id="output"></pre>
|
|
|
|
|
</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>
|
|
|
|
|
<script>
|
|
|
|
|
const ideApi = window.gitZoneIde;
|
|
|
|
|
const elements = {
|
|
|
|
|
workbench: document.getElementById('workbench'),
|
|
|
|
|
activityButtons: Array.from(document.querySelectorAll('.activity-button[data-view]')),
|
|
|
|
|
sidebarPanels: Array.from(document.querySelectorAll('[data-sidebar-panel]')),
|
|
|
|
|
sidebarResizer: document.getElementById('sidebarResizer'),
|
|
|
|
|
panelResizer: document.getElementById('panelResizer'),
|
|
|
|
|
projectTabs: document.getElementById('projectTabs'),
|
|
|
|
|
savedHost: document.getElementById('savedHost'),
|
|
|
|
|
hostList: document.getElementById('hostList'),
|
|
|
|
|
emptyState: document.getElementById('emptyState'),
|
|
|
|
|
explorerHostStatus: document.getElementById('explorerHostStatus'),
|
|
|
|
|
opencodeStatus: document.getElementById('opencodeStatus'),
|
|
|
|
|
opencodeSession: document.getElementById('opencodeSession'),
|
|
|
|
|
opencodePermissions: document.getElementById('opencodePermissions'),
|
|
|
|
|
opencodeMessages: document.getElementById('opencodeMessages'),
|
|
|
|
|
opencodePrompt: document.getElementById('opencodePrompt'),
|
|
|
|
|
projectList: document.getElementById('projectList'),
|
|
|
|
|
projectEmptyState: document.getElementById('projectEmptyState'),
|
|
|
|
|
projectCards: document.getElementById('projectCards'),
|
|
|
|
|
configPath: document.getElementById('configPath'),
|
|
|
|
|
titlebarHost: document.getElementById('titlebarHost'),
|
|
|
|
|
mainHeader: document.getElementById('mainHeader'),
|
|
|
|
|
mainHeaderTitle: document.getElementById('mainHeaderTitle'),
|
|
|
|
|
expandMainPanel: document.getElementById('expandMainPanel'),
|
|
|
|
|
restoreMainPanelFooter: document.getElementById('restoreMainPanelFooter'),
|
|
|
|
|
connectView: document.getElementById('connectView'),
|
|
|
|
|
dashboardView: document.getElementById('dashboardView'),
|
|
|
|
|
frameView: document.getElementById('frameView'),
|
|
|
|
@@ -1347,6 +1580,23 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
let openTabs = [];
|
|
|
|
|
let activeTabId = 'dashboard';
|
|
|
|
|
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 selectedHost = () => hosts.find((host) => host.alias === elements.savedHost.value);
|
|
|
|
@@ -1369,6 +1619,9 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
if (ideApi.onConnectProgress) {
|
|
|
|
|
ideApi.onConnectProgress((message) => appendOutput(String(message)));
|
|
|
|
|
}
|
|
|
|
|
if (ideApi.openCode?.onEvent) {
|
|
|
|
|
ideApi.openCode.onEvent((payload) => handleOpenCodeEvent(payload));
|
|
|
|
|
}
|
|
|
|
|
const formPayload = () => ({
|
|
|
|
|
hostAlias: 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,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 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 = () => {
|
|
|
|
|
elements.explorerHostStatus.textContent = connection
|
|
|
|
|
? '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.frameView.hidden = !tab;
|
|
|
|
|
if (tab) {
|
|
|
|
|
projectFrameDomReady = false;
|
|
|
|
|
elements.projectFrame.src = tab.url;
|
|
|
|
|
elements.mainHeader.textContent = tab.title + ' - ' + tab.path;
|
|
|
|
|
elements.mainHeaderTitle.textContent = tab.title + ' - ' + tab.path;
|
|
|
|
|
scheduleProjectFrameLayoutSync();
|
|
|
|
|
} else if (connection) {
|
|
|
|
|
elements.projectFrame.removeAttribute('src');
|
|
|
|
|
elements.mainHeader.textContent = 'Remote Project Dashboard';
|
|
|
|
|
elements.mainHeaderTitle.textContent = 'Remote Project Dashboard';
|
|
|
|
|
if (layoutState.mainExpanded) {
|
|
|
|
|
setMainExpanded(false);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
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();
|
|
|
|
|
renderProjects();
|
|
|
|
|
void refreshOpenCodeForActiveProject(!!tab);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeTab = (tabId) => {
|
|
|
|
@@ -1608,6 +2223,71 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
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', () => {
|
|
|
|
|
elements.output.textContent = '';
|
|
|
|
|
elements.statusText.textContent = 'Ready';
|
|
|
|
@@ -1684,6 +2364,13 @@ const renderLauncherHtml = () => `<!doctype html>
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
loadLayoutState();
|
|
|
|
|
if (layoutState.mainExpanded && !activeProjectTab()) {
|
|
|
|
|
layoutState.mainExpanded = false;
|
|
|
|
|
saveLayoutState();
|
|
|
|
|
}
|
|
|
|
|
applyLayoutState();
|
|
|
|
|
renderMainPanelControls();
|
|
|
|
|
renderTabs();
|
|
|
|
|
renderProjects();
|
|
|
|
|
activateActivityView('hosts');
|
|
|
|
|