Files
opencode-obsidian/main.js
2026-01-11 14:06:34 +01:00

759 lines
24 KiB
JavaScript

/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
If you want to view the source, please visit the GitHub repository.
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => OpenCodePlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian4 = require("obsidian");
// src/types.ts
var DEFAULT_SETTINGS = {
port: 14096,
hostname: "127.0.0.1",
autoStart: false,
opencodePath: "opencode",
projectDirectory: "",
startupTimeout: 15e3,
defaultViewLocation: "sidebar"
};
var OPENCODE_VIEW_TYPE = "opencode-view";
// src/OpenCodeView.ts
var import_obsidian2 = require("obsidian");
// src/icons.ts
var import_obsidian = require("obsidian");
var OPENCODE_ICON_NAME = "opencode-logo";
var OPENCODE_LOGO_SVG = `<svg viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 24H6V12H18V24Z" fill="currentColor" opacity="0.4"/>
<path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor"/>
</svg>`;
function registerOpenCodeIcons() {
(0, import_obsidian.addIcon)(OPENCODE_ICON_NAME, OPENCODE_LOGO_SVG);
}
// src/OpenCodeView.ts
var OpenCodeView = class extends import_obsidian2.ItemView {
constructor(leaf, plugin) {
super(leaf);
this.iframeEl = null;
this.currentState = "stopped";
this.unsubscribeStateChange = null;
this.plugin = plugin;
}
getViewType() {
return OPENCODE_VIEW_TYPE;
}
getDisplayText() {
return "OpenCode";
}
getIcon() {
return OPENCODE_ICON_NAME;
}
async onOpen() {
this.contentEl.empty();
this.contentEl.addClass("opencode-container");
this.unsubscribeStateChange = this.plugin.onProcessStateChange((state) => {
this.currentState = state;
this.updateView();
});
this.currentState = this.plugin.getProcessState();
this.updateView();
if (this.currentState === "stopped") {
this.plugin.startServer();
}
}
async onClose() {
if (this.unsubscribeStateChange) {
this.unsubscribeStateChange();
this.unsubscribeStateChange = null;
}
if (this.iframeEl) {
this.iframeEl.src = "about:blank";
this.iframeEl = null;
}
}
updateView() {
switch (this.currentState) {
case "stopped":
this.renderStoppedState();
break;
case "starting":
this.renderStartingState();
break;
case "running":
this.renderRunningState();
break;
case "error":
this.renderErrorState();
break;
}
}
renderStoppedState() {
this.contentEl.empty();
const statusContainer = this.contentEl.createDiv({
cls: "opencode-status-container"
});
const iconEl = statusContainer.createDiv({ cls: "opencode-status-icon" });
(0, import_obsidian2.setIcon)(iconEl, "power-off");
statusContainer.createEl("h3", { text: "OpenCode is stopped" });
statusContainer.createEl("p", {
text: "Click the button below to start the OpenCode server.",
cls: "opencode-status-message"
});
const startButton = statusContainer.createEl("button", {
text: "Start OpenCode",
cls: "mod-cta"
});
startButton.addEventListener("click", () => {
this.plugin.startServer();
});
}
renderStartingState() {
this.contentEl.empty();
const statusContainer = this.contentEl.createDiv({
cls: "opencode-status-container"
});
const loadingEl = statusContainer.createDiv({ cls: "opencode-loading" });
loadingEl.createDiv({ cls: "opencode-spinner" });
statusContainer.createEl("h3", { text: "Starting OpenCode..." });
statusContainer.createEl("p", {
text: "Please wait while the server starts up.",
cls: "opencode-status-message"
});
}
renderRunningState() {
this.contentEl.empty();
const headerEl = this.contentEl.createDiv({ cls: "opencode-header" });
const titleSection = headerEl.createDiv({ cls: "opencode-header-title" });
const iconEl = titleSection.createSpan();
(0, import_obsidian2.setIcon)(iconEl, OPENCODE_ICON_NAME);
titleSection.createSpan({ text: "OpenCode" });
const actionsEl = headerEl.createDiv({ cls: "opencode-header-actions" });
const reloadButton = actionsEl.createEl("button", {
attr: { "aria-label": "Reload" }
});
(0, import_obsidian2.setIcon)(reloadButton, "refresh-cw");
reloadButton.addEventListener("click", () => {
this.reloadIframe();
});
const stopButton = actionsEl.createEl("button", {
attr: { "aria-label": "Stop server" }
});
(0, import_obsidian2.setIcon)(stopButton, "square");
stopButton.addEventListener("click", () => {
this.plugin.stopServer();
});
const iframeContainer = this.contentEl.createDiv({
cls: "opencode-iframe-container"
});
console.log("[OpenCode] Loading iframe with URL:", this.plugin.getServerUrl());
this.iframeEl = iframeContainer.createEl("iframe", {
cls: "opencode-iframe",
attr: {
src: this.plugin.getServerUrl(),
frameborder: "0",
allow: "clipboard-read; clipboard-write"
}
});
this.iframeEl.addEventListener("error", () => {
console.error("Failed to load OpenCode iframe");
});
}
renderErrorState() {
this.contentEl.empty();
const statusContainer = this.contentEl.createDiv({
cls: "opencode-status-container opencode-error"
});
const iconEl = statusContainer.createDiv({ cls: "opencode-status-icon" });
(0, import_obsidian2.setIcon)(iconEl, "alert-circle");
statusContainer.createEl("h3", { text: "Failed to start OpenCode" });
const errorMessage = this.plugin.getLastError();
if (errorMessage) {
statusContainer.createEl("p", {
text: errorMessage,
cls: "opencode-status-message opencode-error-message"
});
} else {
statusContainer.createEl("p", {
text: "There was an error starting the OpenCode server.",
cls: "opencode-status-message"
});
}
const buttonContainer = statusContainer.createDiv({
cls: "opencode-button-group"
});
const retryButton = buttonContainer.createEl("button", {
text: "Retry",
cls: "mod-cta"
});
retryButton.addEventListener("click", () => {
this.plugin.startServer();
});
const settingsButton = buttonContainer.createEl("button", {
text: "Open Settings"
});
settingsButton.addEventListener("click", () => {
this.app.setting.open();
this.app.setting.openTabById("obsidian-opencode");
});
}
reloadIframe() {
if (this.iframeEl) {
const src = this.iframeEl.src;
this.iframeEl.src = "about:blank";
setTimeout(() => {
if (this.iframeEl) {
this.iframeEl.src = src;
}
}, 100);
}
}
};
// src/SettingsTab.ts
var import_obsidian3 = require("obsidian");
var import_fs = require("fs");
var import_os = require("os");
function expandTilde(path) {
if (path === "~") {
return (0, import_os.homedir)();
}
if (path.startsWith("~/")) {
return path.replace("~", (0, import_os.homedir)());
}
return path;
}
var OpenCodeSettingTab = class extends import_obsidian3.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.validateTimeout = null;
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "OpenCode Settings" });
containerEl.createEl("h3", { text: "Server Configuration" });
new import_obsidian3.Setting(containerEl).setName("Port").setDesc("Port number for the OpenCode web server").addText(
(text) => text.setPlaceholder("14096").setValue(this.plugin.settings.port.toString()).onChange(async (value) => {
const port = parseInt(value, 10);
if (!isNaN(port) && port > 0 && port < 65536) {
this.plugin.settings.port = port;
await this.plugin.saveSettings();
}
})
);
new import_obsidian3.Setting(containerEl).setName("Hostname").setDesc("Hostname to bind the server to (usually 127.0.0.1)").addText(
(text) => text.setPlaceholder("127.0.0.1").setValue(this.plugin.settings.hostname).onChange(async (value) => {
this.plugin.settings.hostname = value || "127.0.0.1";
await this.plugin.saveSettings();
})
);
new import_obsidian3.Setting(containerEl).setName("OpenCode path").setDesc(
"Path to the OpenCode executable. Leave as 'opencode' if it's in your PATH."
).addText(
(text) => text.setPlaceholder("opencode").setValue(this.plugin.settings.opencodePath).onChange(async (value) => {
this.plugin.settings.opencodePath = value || "opencode";
await this.plugin.saveSettings();
})
);
new import_obsidian3.Setting(containerEl).setName("Project directory").setDesc(
"Override the starting directory for OpenCode. Leave empty to use the vault root. Supports ~ for home directory."
).addText(
(text) => text.setPlaceholder("/path/to/project or ~/project").setValue(this.plugin.settings.projectDirectory).onChange((value) => {
if (this.validateTimeout) {
clearTimeout(this.validateTimeout);
}
this.validateTimeout = setTimeout(async () => {
await this.validateAndSetProjectDirectory(value);
}, 500);
})
);
containerEl.createEl("h3", { text: "Behavior" });
new import_obsidian3.Setting(containerEl).setName("Auto-start server").setDesc(
"Automatically start the OpenCode server when Obsidian opens (not recommended for faster startup)"
).addToggle(
(toggle) => toggle.setValue(this.plugin.settings.autoStart).onChange(async (value) => {
this.plugin.settings.autoStart = value;
await this.plugin.saveSettings();
})
);
new import_obsidian3.Setting(containerEl).setName("Default view location").setDesc(
"Where to open the OpenCode panel: sidebar opens in the right panel, main opens as a tab in the editor area"
).addDropdown(
(dropdown) => dropdown.addOption("sidebar", "Sidebar").addOption("main", "Main window").setValue(this.plugin.settings.defaultViewLocation).onChange(async (value) => {
this.plugin.settings.defaultViewLocation = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl("h3", { text: "Server Status" });
const statusContainer = containerEl.createDiv({ cls: "opencode-settings-status" });
this.renderServerStatus(statusContainer);
}
async validateAndSetProjectDirectory(value) {
const trimmed = value.trim();
if (!trimmed) {
await this.plugin.updateProjectDirectory("");
return;
}
if (!trimmed.startsWith("/") && !trimmed.startsWith("~") && !trimmed.match(/^[A-Za-z]:\\/)) {
new import_obsidian3.Notice("Project directory must be an absolute path (or start with ~)");
return;
}
const expanded = expandTilde(trimmed);
try {
if (!(0, import_fs.existsSync)(expanded)) {
new import_obsidian3.Notice("Project directory does not exist");
return;
}
const stat = (0, import_fs.statSync)(expanded);
if (!stat.isDirectory()) {
new import_obsidian3.Notice("Project directory path is not a directory");
return;
}
} catch (error) {
new import_obsidian3.Notice(`Failed to validate path: ${error.message}`);
return;
}
await this.plugin.updateProjectDirectory(expanded);
}
renderServerStatus(container) {
container.empty();
const state = this.plugin.getProcessState();
const statusText = {
stopped: "Stopped",
starting: "Starting...",
running: "Running",
error: "Error"
};
const statusClass = {
stopped: "status-stopped",
starting: "status-starting",
running: "status-running",
error: "status-error"
};
const statusEl = container.createDiv({ cls: "opencode-status-line" });
statusEl.createSpan({ text: "Status: " });
statusEl.createSpan({
text: statusText[state],
cls: `opencode-status-badge ${statusClass[state]}`
});
if (state === "running") {
const urlEl = container.createDiv({ cls: "opencode-status-line" });
urlEl.createSpan({ text: "URL: " });
const linkEl = urlEl.createEl("a", {
text: this.plugin.getServerUrl(),
href: this.plugin.getServerUrl()
});
linkEl.addEventListener("click", (e) => {
e.preventDefault();
window.open(this.plugin.getServerUrl(), "_blank");
});
}
const buttonContainer = container.createDiv({ cls: "opencode-settings-buttons" });
if (state === "stopped" || state === "error") {
const startButton = buttonContainer.createEl("button", {
text: "Start Server",
cls: "mod-cta"
});
startButton.addEventListener("click", async () => {
await this.plugin.startServer();
this.renderServerStatus(container);
});
}
if (state === "running") {
const stopButton = buttonContainer.createEl("button", {
text: "Stop Server"
});
stopButton.addEventListener("click", () => {
this.plugin.stopServer();
this.renderServerStatus(container);
});
const restartButton = buttonContainer.createEl("button", {
text: "Restart Server",
cls: "mod-warning"
});
restartButton.addEventListener("click", async () => {
this.plugin.stopServer();
await this.plugin.startServer();
this.renderServerStatus(container);
});
}
if (state === "starting") {
buttonContainer.createSpan({
text: "Please wait...",
cls: "opencode-status-waiting"
});
}
}
};
// src/ProcessManager.ts
var import_child_process = require("child_process");
var ProcessManager = class {
constructor(settings, projectDirectory, onStateChange) {
this.process = null;
this.state = "stopped";
this.lastError = null;
this.earlyExitCode = null;
this.settings = settings;
this.projectDirectory = projectDirectory;
this.onStateChange = onStateChange;
}
updateSettings(settings) {
this.settings = settings;
}
updateProjectDirectory(directory) {
this.projectDirectory = directory;
}
getState() {
return this.state;
}
getLastError() {
return this.lastError;
}
getUrl() {
const baseUrl = `http://${this.settings.hostname}:${this.settings.port}`;
const encodedPath = btoa(this.projectDirectory);
return `${baseUrl}/${encodedPath}`;
}
async start() {
var _a, _b;
if (this.state === "running" || this.state === "starting") {
return true;
}
this.setState("starting");
this.lastError = null;
this.earlyExitCode = null;
if (!this.projectDirectory) {
return this.setError("Project directory (vault) not configured");
}
if (await this.checkServerHealth()) {
console.log("[OpenCode] Server already running on port", this.settings.port);
this.setState("running");
return true;
}
console.log("[OpenCode] Starting server:", {
opencodePath: this.settings.opencodePath,
port: this.settings.port,
hostname: this.settings.hostname,
cwd: this.projectDirectory,
projectDirectory: this.projectDirectory
});
this.process = (0, import_child_process.spawn)(
this.settings.opencodePath,
[
"serve",
"--port",
this.settings.port.toString(),
"--hostname",
this.settings.hostname,
"--cors",
"app://obsidian.md"
],
{
cwd: this.projectDirectory,
env: { ...process.env },
stdio: ["ignore", "pipe", "pipe"],
detached: false
}
);
console.log("[OpenCode] Process spawned with PID:", this.process.pid);
(_a = this.process.stdout) == null ? void 0 : _a.on("data", (data) => {
console.log("[OpenCode]", data.toString().trim());
});
(_b = this.process.stderr) == null ? void 0 : _b.on("data", (data) => {
console.error("[OpenCode Error]", data.toString().trim());
});
this.process.on("exit", (code, signal) => {
console.log(`[OpenCode] Process exited with code ${code}, signal ${signal}`);
this.process = null;
if (this.state === "starting" && code !== null && code !== 0) {
this.earlyExitCode = code;
}
if (this.state === "running") {
this.setState("stopped");
}
});
this.process.on("error", (err) => {
console.error("[OpenCode] Failed to start process:", err);
this.process = null;
if (err.code === "ENOENT") {
this.setError(`Executable not found at '${this.settings.opencodePath}'`);
} else {
this.setError(`Failed to start: ${err.message}`);
}
});
const ready = await this.waitForServerOrExit(this.settings.startupTimeout);
if (ready) {
this.setState("running");
return true;
}
if (this.state === "error") {
return false;
}
this.stop();
if (this.earlyExitCode !== null) {
return this.setError(`Process exited unexpectedly (exit code ${this.earlyExitCode})`);
}
if (!this.process) {
return this.setError("Process exited before server became ready");
}
return this.setError("Server failed to start within timeout");
}
stop() {
if (!this.process) {
this.setState("stopped");
return;
}
const proc = this.process;
console.log("[OpenCode] Stopping process with PID:", proc.pid);
this.setState("stopped");
this.process = null;
proc.kill("SIGTERM");
setTimeout(() => {
if (proc.exitCode === null && proc.signalCode === null) {
console.log("[OpenCode] Process still running, sending SIGKILL");
proc.kill("SIGKILL");
}
}, 2e3);
}
setState(state) {
this.state = state;
this.onStateChange(state);
}
setError(message) {
this.lastError = message;
console.error("[OpenCode Error]", message);
this.setState("error");
return false;
}
async checkServerHealth() {
try {
const response = await fetch(`${this.getUrl()}/global/health`, {
method: "GET",
signal: AbortSignal.timeout(2e3)
});
return response.ok;
} catch (e) {
return false;
}
}
async waitForServerOrExit(timeoutMs) {
const startTime = Date.now();
const pollInterval = 500;
while (Date.now() - startTime < timeoutMs) {
if (!this.process) {
console.log("[OpenCode] Process exited before server became ready");
return false;
}
if (await this.checkServerHealth()) {
return true;
}
await this.sleep(pollInterval);
}
return false;
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
// src/main.ts
var OpenCodePlugin = class extends import_obsidian4.Plugin {
constructor() {
super(...arguments);
this.settings = DEFAULT_SETTINGS;
this.stateChangeCallbacks = [];
}
async onload() {
console.log("Loading OpenCode plugin");
registerOpenCodeIcons();
await this.loadSettings();
const projectDirectory = this.getProjectDirectory();
this.processManager = new ProcessManager(
this.settings,
projectDirectory,
(state) => this.notifyStateChange(state)
);
console.log("[OpenCode] Configured with project directory:", projectDirectory);
this.registerView(OPENCODE_VIEW_TYPE, (leaf) => new OpenCodeView(leaf, this));
this.addSettingTab(new OpenCodeSettingTab(this.app, this));
this.addRibbonIcon(OPENCODE_ICON_NAME, "OpenCode", () => {
this.activateView();
});
this.addCommand({
id: "toggle-opencode-view",
name: "Toggle OpenCode panel",
callback: () => {
this.toggleView();
},
hotkeys: [
{
modifiers: ["Mod", "Shift"],
key: "o"
}
]
});
this.addCommand({
id: "start-opencode-server",
name: "Start OpenCode server",
callback: () => {
this.startServer();
}
});
this.addCommand({
id: "stop-opencode-server",
name: "Stop OpenCode server",
callback: () => {
this.stopServer();
}
});
if (this.settings.autoStart) {
this.app.workspace.onLayoutReady(async () => {
await this.startServer();
});
}
console.log("OpenCode plugin loaded");
}
async onunload() {
this.stopServer();
this.app.workspace.detachLeavesOfType(OPENCODE_VIEW_TYPE);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
this.processManager.updateSettings(this.settings);
}
// Update project directory and restart server if running
async updateProjectDirectory(directory) {
this.settings.projectDirectory = directory;
await this.saveData(this.settings);
this.processManager.updateProjectDirectory(this.getProjectDirectory());
if (this.getProcessState() === "running") {
this.stopServer();
await this.startServer();
}
}
// Get existing view leaf if any
getExistingLeaf() {
const leaves = this.app.workspace.getLeavesOfType(OPENCODE_VIEW_TYPE);
return leaves.length > 0 ? leaves[0] : null;
}
// Activate or create the view
async activateView() {
const existingLeaf = this.getExistingLeaf();
if (existingLeaf) {
this.app.workspace.revealLeaf(existingLeaf);
return;
}
let leaf = null;
if (this.settings.defaultViewLocation === "main") {
leaf = this.app.workspace.getLeaf("tab");
} else {
leaf = this.app.workspace.getRightLeaf(false);
}
if (leaf) {
await leaf.setViewState({
type: OPENCODE_VIEW_TYPE,
active: true
});
this.app.workspace.revealLeaf(leaf);
}
}
// Toggle view visibility
async toggleView() {
const existingLeaf = this.getExistingLeaf();
if (existingLeaf) {
const isInSidebar = existingLeaf.getRoot() === this.app.workspace.rightSplit;
if (isInSidebar) {
const rightSplit = this.app.workspace.rightSplit;
if (rightSplit && !rightSplit.collapsed) {
existingLeaf.detach();
} else {
this.app.workspace.revealLeaf(existingLeaf);
}
} else {
existingLeaf.detach();
}
} else {
await this.activateView();
}
}
async startServer() {
const success = await this.processManager.start();
if (success) {
new import_obsidian4.Notice("OpenCode server started");
}
return success;
}
stopServer() {
this.processManager.stop();
new import_obsidian4.Notice("OpenCode server stopped");
}
getProcessState() {
var _a, _b;
return (_b = (_a = this.processManager) == null ? void 0 : _a.getState()) != null ? _b : "stopped";
}
getLastError() {
var _a;
return (_a = this.processManager.getLastError()) != null ? _a : null;
}
getServerUrl() {
return this.processManager.getUrl();
}
onProcessStateChange(callback) {
this.stateChangeCallbacks.push(callback);
return () => {
const index = this.stateChangeCallbacks.indexOf(callback);
if (index > -1) {
this.stateChangeCallbacks.splice(index, 1);
}
};
}
notifyStateChange(state) {
for (const callback of this.stateChangeCallbacks) {
callback(state);
}
}
getProjectDirectory() {
if (this.settings.projectDirectory) {
console.log("[OpenCode] Using project directory from settings:", this.settings.projectDirectory);
return this.settings.projectDirectory;
}
const adapter = this.app.vault.adapter;
const vaultPath = adapter.basePath || "";
if (!vaultPath) {
console.warn("[OpenCode] Warning: Could not determine vault path");
}
console.log("[OpenCode] Using vault path as project directory:", vaultPath);
return vaultPath;
}
};