1218 lines
39 KiB
JavaScript
1218 lines
39 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_obsidian5 = require("obsidian");
|
|
|
|
// src/types.ts
|
|
var DEFAULT_SETTINGS = {
|
|
port: 14096,
|
|
hostname: "127.0.0.1",
|
|
autoStart: false,
|
|
opencodePath: "opencode",
|
|
projectDirectory: "",
|
|
startupTimeout: 15e3,
|
|
defaultViewLocation: "sidebar",
|
|
injectWorkspaceContext: true,
|
|
maxNotesInContext: 20,
|
|
maxSelectionLength: 2e3
|
|
};
|
|
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) {
|
|
const iframeUrl = this.iframeEl.src;
|
|
if (iframeUrl.includes("/session/")) {
|
|
this.plugin.setCachedIframeUrl(iframeUrl);
|
|
}
|
|
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() {
|
|
var _a;
|
|
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"
|
|
});
|
|
const iframeUrl = (_a = this.plugin.getStoredIframeUrl()) != null ? _a : this.plugin.getServerUrl();
|
|
console.log("[OpenCode] Loading iframe with URL:", iframeUrl);
|
|
this.iframeEl = iframeContainer.createEl("iframe", {
|
|
cls: "opencode-iframe",
|
|
attr: {
|
|
src: iframeUrl,
|
|
frameborder: "0",
|
|
allow: "clipboard-read; clipboard-write"
|
|
}
|
|
});
|
|
this.iframeEl.addEventListener("error", () => {
|
|
console.error("Failed to load OpenCode iframe");
|
|
});
|
|
this.iframeEl.addEventListener("focus", () => {
|
|
this.plugin.refreshContextForView(this);
|
|
});
|
|
this.iframeEl.addEventListener("pointerdown", () => {
|
|
this.plugin.refreshContextForView(this);
|
|
});
|
|
void this.plugin.ensureSessionUrl(this);
|
|
}
|
|
getIframeUrl() {
|
|
var _a, _b;
|
|
return (_b = (_a = this.iframeEl) == null ? void 0 : _a.src) != null ? _b : null;
|
|
}
|
|
setIframeUrl(url) {
|
|
if (this.iframeEl && this.iframeEl.src !== url) {
|
|
this.iframeEl.src = url;
|
|
}
|
|
}
|
|
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: "Workspace Context" });
|
|
new import_obsidian3.Setting(containerEl).setName("Inject workspace context").setDesc(
|
|
"Includes open note paths and selected text in OpenCode when the view is focused"
|
|
).addToggle(
|
|
(toggle) => toggle.setValue(this.plugin.settings.injectWorkspaceContext).onChange(async (value) => {
|
|
this.plugin.settings.injectWorkspaceContext = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian3.Setting(containerEl).setName("Max notes in context").setDesc("Limit how many open notes are included").addSlider(
|
|
(slider) => slider.setLimits(1, 50, 1).setValue(this.plugin.settings.maxNotesInContext).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.maxNotesInContext = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian3.Setting(containerEl).setName("Max selection length").setDesc("Truncate selected text to avoid oversized context").addSlider(
|
|
(slider) => slider.setLimits(500, 5e3, 100).setValue(this.plugin.settings.maxSelectionLength).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.maxSelectionLength = 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 encodedPath = btoa(this.projectDirectory);
|
|
return `http://${this.settings.hostname}:${this.settings.port}/${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, NODE_USE_SYSTEM_CA: "1" },
|
|
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/OpenCodeClient.ts
|
|
var OpenCodeClient = class {
|
|
constructor(apiBaseUrl, uiBaseUrl, projectDirectory) {
|
|
this.trackedSessionId = null;
|
|
this.lastPart = null;
|
|
this.apiBaseUrl = this.normalizeBaseUrl(apiBaseUrl);
|
|
this.uiBaseUrl = this.normalizeBaseUrl(uiBaseUrl);
|
|
this.projectDirectory = projectDirectory;
|
|
}
|
|
updateBaseUrl(apiBaseUrl, uiBaseUrl, projectDirectory) {
|
|
const nextApiUrl = this.normalizeBaseUrl(apiBaseUrl);
|
|
const nextUiUrl = this.normalizeBaseUrl(uiBaseUrl);
|
|
if (nextApiUrl !== this.apiBaseUrl || nextUiUrl !== this.uiBaseUrl || projectDirectory !== this.projectDirectory) {
|
|
this.apiBaseUrl = nextApiUrl;
|
|
this.uiBaseUrl = nextUiUrl;
|
|
this.projectDirectory = projectDirectory;
|
|
this.resetTracking();
|
|
}
|
|
}
|
|
resetTracking() {
|
|
this.trackedSessionId = null;
|
|
this.lastPart = null;
|
|
}
|
|
getSessionUrl(sessionId) {
|
|
return `${this.uiBaseUrl}/session/${sessionId}`;
|
|
}
|
|
resolveSessionId(iframeUrl) {
|
|
var _a;
|
|
const match = iframeUrl.match(/\/session\/([^/?#]+)/);
|
|
return (_a = match == null ? void 0 : match[1]) != null ? _a : null;
|
|
}
|
|
async createSession() {
|
|
var _a;
|
|
const result = await this.request("POST", "/session", {
|
|
title: "Obsidian"
|
|
});
|
|
const session = this.unwrap(result);
|
|
return (_a = session == null ? void 0 : session.id) != null ? _a : null;
|
|
}
|
|
async updateContext(params) {
|
|
var _a, _b, _c;
|
|
const { sessionId, contextText } = params;
|
|
if (this.trackedSessionId && this.trackedSessionId !== sessionId) {
|
|
this.resetTracking();
|
|
}
|
|
this.trackedSessionId = sessionId;
|
|
if (!contextText) {
|
|
await this.ignorePreviousPart();
|
|
return;
|
|
}
|
|
if (this.lastPart) {
|
|
const updated = await this.updatePart(this.lastPart, { text: contextText });
|
|
if (updated) {
|
|
return;
|
|
}
|
|
await this.ignorePreviousPart();
|
|
}
|
|
const message = await this.sendPrompt(sessionId, contextText);
|
|
if ((_a = message == null ? void 0 : message.info) == null ? void 0 : _a.id) {
|
|
this.lastPart = (_c = (_b = message.parts) == null ? void 0 : _b[0]) != null ? _c : null;
|
|
}
|
|
}
|
|
async sendPrompt(sessionId, contextText) {
|
|
const result = await this.request(
|
|
"POST",
|
|
`/session/${sessionId}/message`,
|
|
{
|
|
noReply: true,
|
|
parts: [{ type: "text", text: contextText }]
|
|
}
|
|
);
|
|
console.log("[OpenCode] Injected context message");
|
|
console.log(contextText);
|
|
const message = this.unwrap(result);
|
|
if (!message) {
|
|
console.error("[OpenCode] Failed to inject context message");
|
|
}
|
|
return message;
|
|
}
|
|
async updatePart(part, updates) {
|
|
const result = await this.request(
|
|
"PATCH",
|
|
`/session/${part.sessionID}/message/${part.messageID}/part/${part.id}`,
|
|
{
|
|
...part,
|
|
...updates
|
|
}
|
|
);
|
|
const updated = this.unwrap(result);
|
|
if (updated) {
|
|
this.lastPart = updated;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
async ignorePreviousPart() {
|
|
if (!this.lastPart) {
|
|
return false;
|
|
}
|
|
const ignored = await this.updatePart(this.lastPart, { ignored: true });
|
|
if (!ignored) {
|
|
return false;
|
|
}
|
|
this.lastPart = null;
|
|
return true;
|
|
}
|
|
async request(method, path, body) {
|
|
try {
|
|
const url = `${this.apiBaseUrl}${path}`;
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-opencode-directory": this.projectDirectory
|
|
},
|
|
body: body ? JSON.stringify(body) : void 0
|
|
});
|
|
if (!response.ok) {
|
|
console.error("[OpenCode] API request failed", {
|
|
path,
|
|
status: response.status
|
|
});
|
|
return null;
|
|
}
|
|
const json = await response.json().catch(() => null);
|
|
return json;
|
|
} catch (error) {
|
|
console.error("[OpenCode] API request error", error);
|
|
return null;
|
|
}
|
|
}
|
|
unwrap(result) {
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
if (typeof result === "object") {
|
|
const payload = result;
|
|
if (payload.data) {
|
|
return payload.data;
|
|
}
|
|
if (payload.message) {
|
|
return payload.message;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
normalizeBaseUrl(baseUrl) {
|
|
return baseUrl.replace(/\/+$/, "");
|
|
}
|
|
};
|
|
|
|
// src/WorkspaceContext.ts
|
|
var import_obsidian4 = require("obsidian");
|
|
var WorkspaceContext = class {
|
|
constructor(app) {
|
|
this.lastSelection = null;
|
|
this.lastMarkdownView = null;
|
|
this.app = app;
|
|
}
|
|
trackViewSelection(view) {
|
|
var _a, _b, _c;
|
|
if (view) {
|
|
this.lastMarkdownView = view;
|
|
}
|
|
const sourcePath = (_a = view == null ? void 0 : view.file) == null ? void 0 : _a.path;
|
|
const selection = (_c = (_b = view == null ? void 0 : view.editor) == null ? void 0 : _b.getSelection()) != null ? _c : "";
|
|
if (sourcePath && selection.trim()) {
|
|
this.lastSelection = {
|
|
text: selection,
|
|
sourcePath
|
|
};
|
|
}
|
|
}
|
|
gatherContext(maxNotes, maxSelectionLength) {
|
|
var _a, _b, _c, _d, _e;
|
|
const leaves = this.app.workspace.getLeavesOfType("markdown");
|
|
const paths = /* @__PURE__ */ new Set();
|
|
for (const leaf of leaves) {
|
|
const view2 = leaf.view;
|
|
const path = (_a = view2.file) == null ? void 0 : _a.path;
|
|
if (path) {
|
|
paths.add(path);
|
|
}
|
|
}
|
|
const openNotePaths = Array.from(paths).slice(0, Math.max(0, maxNotes));
|
|
const view = (_b = this.app.workspace.getActiveViewOfType(import_obsidian4.MarkdownView)) != null ? _b : this.lastMarkdownView;
|
|
this.trackViewSelection(view);
|
|
const sourcePath = (_c = view == null ? void 0 : view.file) == null ? void 0 : _c.path;
|
|
const selection = (_e = (_d = view == null ? void 0 : view.editor) == null ? void 0 : _d.getSelection()) != null ? _e : "";
|
|
let selectionContext = null;
|
|
if (sourcePath && selection.trim()) {
|
|
selectionContext = {
|
|
text: selection,
|
|
sourcePath
|
|
};
|
|
this.lastSelection = selectionContext;
|
|
} else if (this.lastSelection) {
|
|
selectionContext = this.lastSelection;
|
|
}
|
|
if (selectionContext && selectionContext.text.length > maxSelectionLength) {
|
|
selectionContext = {
|
|
...selectionContext,
|
|
text: selectionContext.text.slice(0, maxSelectionLength) + "... [truncated]"
|
|
};
|
|
}
|
|
let contextText = null;
|
|
if (openNotePaths.length > 0 || selectionContext) {
|
|
const lines = ["<obsidian-context>"];
|
|
if (openNotePaths.length > 0) {
|
|
lines.push("Currently open notes in Obsidian:");
|
|
for (const path of openNotePaths) {
|
|
lines.push(`- ${path}`);
|
|
}
|
|
}
|
|
if (selectionContext) {
|
|
lines.push("");
|
|
lines.push(`Selected text (from ${selectionContext.sourcePath}):`);
|
|
lines.push('"""');
|
|
lines.push(selectionContext.text);
|
|
lines.push('"""');
|
|
}
|
|
lines.push("</obsidian-context>");
|
|
contextText = lines.join("\n");
|
|
}
|
|
return {
|
|
openNotePaths,
|
|
selection: selectionContext,
|
|
contextText
|
|
};
|
|
}
|
|
};
|
|
|
|
// src/main.ts
|
|
var OpenCodePlugin = class extends import_obsidian5.Plugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.settings = DEFAULT_SETTINGS;
|
|
this.stateChangeCallbacks = [];
|
|
this.cachedIframeUrl = null;
|
|
this.lastBaseUrl = null;
|
|
this.contextEventRefs = [];
|
|
this.contextRefreshTimer = null;
|
|
}
|
|
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)
|
|
);
|
|
this.openCodeClient = new OpenCodeClient(this.getApiBaseUrl(), this.getServerUrl(), projectDirectory);
|
|
this.workspaceContext = new WorkspaceContext(this.app);
|
|
this.lastBaseUrl = this.getServerUrl();
|
|
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();
|
|
});
|
|
}
|
|
this.updateContextListeners();
|
|
this.onProcessStateChange((state) => {
|
|
if (state === "running") {
|
|
void this.handleServerRunning();
|
|
}
|
|
});
|
|
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);
|
|
this.refreshClientState();
|
|
this.updateContextListeners();
|
|
}
|
|
// 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());
|
|
this.refreshClientState();
|
|
if (this.getProcessState() === "running") {
|
|
this.stopServer();
|
|
await this.startServer();
|
|
}
|
|
}
|
|
getExistingLeaf() {
|
|
const leaves = this.app.workspace.getLeavesOfType(OPENCODE_VIEW_TYPE);
|
|
return leaves.length > 0 ? leaves[0] : null;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
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_obsidian5.Notice("OpenCode server started");
|
|
}
|
|
return success;
|
|
}
|
|
stopServer() {
|
|
this.processManager.stop();
|
|
new import_obsidian5.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();
|
|
}
|
|
getApiBaseUrl() {
|
|
return `http://${this.settings.hostname}:${this.settings.port}`;
|
|
}
|
|
getStoredIframeUrl() {
|
|
return this.cachedIframeUrl;
|
|
}
|
|
setCachedIframeUrl(url) {
|
|
this.cachedIframeUrl = url;
|
|
}
|
|
async ensureSessionUrl(view) {
|
|
var _a;
|
|
if (this.getProcessState() !== "running") {
|
|
return;
|
|
}
|
|
const existingUrl = (_a = this.cachedIframeUrl) != null ? _a : view.getIframeUrl();
|
|
if (existingUrl && this.openCodeClient.resolveSessionId(existingUrl)) {
|
|
this.cachedIframeUrl = existingUrl;
|
|
return;
|
|
}
|
|
const sessionId = await this.openCodeClient.createSession();
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
const sessionUrl = this.openCodeClient.getSessionUrl(sessionId);
|
|
this.cachedIframeUrl = sessionUrl;
|
|
view.setIframeUrl(sessionUrl);
|
|
if (this.app.workspace.activeLeaf === view.leaf) {
|
|
await this.updateOpenCodeContext(view.leaf);
|
|
}
|
|
}
|
|
refreshContextForView(view) {
|
|
if (!this.settings.injectWorkspaceContext) {
|
|
return;
|
|
}
|
|
void this.updateOpenCodeContext(view.leaf);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
refreshClientState() {
|
|
const nextUiBaseUrl = this.getServerUrl();
|
|
const nextApiBaseUrl = this.getApiBaseUrl();
|
|
const projectDirectory = this.getProjectDirectory();
|
|
this.openCodeClient.updateBaseUrl(nextApiBaseUrl, nextUiBaseUrl, projectDirectory);
|
|
if (this.lastBaseUrl && this.lastBaseUrl !== nextUiBaseUrl) {
|
|
this.cachedIframeUrl = null;
|
|
}
|
|
this.lastBaseUrl = nextUiBaseUrl;
|
|
}
|
|
updateContextListeners() {
|
|
if (!this.settings.injectWorkspaceContext) {
|
|
this.clearContextListeners();
|
|
return;
|
|
}
|
|
if (this.contextEventRefs.length > 0) {
|
|
return;
|
|
}
|
|
const activeLeafRef = this.app.workspace.on("active-leaf-change", (leaf) => {
|
|
if ((leaf == null ? void 0 : leaf.view) instanceof import_obsidian5.MarkdownView) {
|
|
this.workspaceContext.trackViewSelection(leaf.view);
|
|
}
|
|
this.scheduleContextRefresh(0);
|
|
});
|
|
const fileOpenRef = this.app.workspace.on("file-open", () => {
|
|
this.scheduleContextRefresh();
|
|
});
|
|
const fileCloseRef = this.app.workspace.on("file-close", () => {
|
|
this.scheduleContextRefresh();
|
|
});
|
|
const layoutChangeRef = this.app.workspace.on("layout-change", () => {
|
|
this.scheduleContextRefresh();
|
|
});
|
|
const editorChangeRef = this.app.workspace.on("editor-change", (_editor, view) => {
|
|
if (view instanceof import_obsidian5.MarkdownView) {
|
|
this.workspaceContext.trackViewSelection(view);
|
|
}
|
|
this.scheduleContextRefresh(500);
|
|
});
|
|
const selectionChangeRef = this.app.workspace.on(
|
|
"editor-selection-change",
|
|
(_editor, view) => {
|
|
if (view instanceof import_obsidian5.MarkdownView) {
|
|
this.workspaceContext.trackViewSelection(view);
|
|
}
|
|
this.scheduleContextRefresh(200);
|
|
}
|
|
);
|
|
this.contextEventRefs = [
|
|
activeLeafRef,
|
|
fileOpenRef,
|
|
fileCloseRef,
|
|
layoutChangeRef,
|
|
editorChangeRef,
|
|
selectionChangeRef
|
|
];
|
|
this.contextEventRefs.forEach((ref) => this.registerEvent(ref));
|
|
}
|
|
clearContextListeners() {
|
|
for (const ref of this.contextEventRefs) {
|
|
this.app.workspace.offref(ref);
|
|
}
|
|
this.contextEventRefs = [];
|
|
if (this.contextRefreshTimer !== null) {
|
|
window.clearTimeout(this.contextRefreshTimer);
|
|
this.contextRefreshTimer = null;
|
|
}
|
|
}
|
|
scheduleContextRefresh(delayMs = 300) {
|
|
const leaf = this.getOpenCodeLeafForRefresh();
|
|
if (!leaf) {
|
|
return;
|
|
}
|
|
if (this.contextRefreshTimer !== null) {
|
|
window.clearTimeout(this.contextRefreshTimer);
|
|
}
|
|
this.contextRefreshTimer = window.setTimeout(() => {
|
|
this.contextRefreshTimer = null;
|
|
void this.updateOpenCodeContext(leaf);
|
|
}, delayMs);
|
|
}
|
|
getOpenCodeLeafForRefresh() {
|
|
const activeLeaf = this.app.workspace.activeLeaf;
|
|
if ((activeLeaf == null ? void 0 : activeLeaf.view.getViewType()) === OPENCODE_VIEW_TYPE) {
|
|
return activeLeaf;
|
|
}
|
|
return this.getVisibleSidebarOpenCodeLeaf();
|
|
}
|
|
getVisibleSidebarOpenCodeLeaf() {
|
|
const leaves = this.app.workspace.getLeavesOfType(OPENCODE_VIEW_TYPE);
|
|
if (leaves.length === 0) {
|
|
return null;
|
|
}
|
|
const rightSplit = this.app.workspace.rightSplit;
|
|
if (!rightSplit || rightSplit.collapsed) {
|
|
return null;
|
|
}
|
|
const leaf = leaves[0];
|
|
return leaf.getRoot() === rightSplit ? leaf : null;
|
|
}
|
|
async handleServerRunning() {
|
|
const activeLeaf = this.app.workspace.activeLeaf;
|
|
if ((activeLeaf == null ? void 0 : activeLeaf.view.getViewType()) === OPENCODE_VIEW_TYPE) {
|
|
await this.updateOpenCodeContext(activeLeaf);
|
|
}
|
|
}
|
|
async updateOpenCodeContext(leaf) {
|
|
var _a;
|
|
if (!this.settings.injectWorkspaceContext) {
|
|
return;
|
|
}
|
|
if (this.getProcessState() !== "running") {
|
|
return;
|
|
}
|
|
const view = leaf.view instanceof OpenCodeView ? leaf.view : null;
|
|
const iframeUrl = (_a = this.cachedIframeUrl) != null ? _a : view == null ? void 0 : view.getIframeUrl();
|
|
if (!iframeUrl) {
|
|
return;
|
|
}
|
|
const sessionId = this.openCodeClient.resolveSessionId(iframeUrl);
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
this.cachedIframeUrl = iframeUrl;
|
|
const { contextText } = this.workspaceContext.gatherContext(
|
|
this.settings.maxNotesInContext,
|
|
this.settings.maxSelectionLength
|
|
);
|
|
await this.openCodeClient.updateContext({
|
|
sessionId,
|
|
contextText
|
|
});
|
|
}
|
|
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;
|
|
}
|
|
};
|