diff --git a/.gitignore b/.gitignore
index c2d3004..e12fee6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
-node_modules
-data.json
+/node_modules
+/data.json
+/main.js
diff --git a/main.js b/main.js
deleted file mode 100644
index 2b65a9f..0000000
--- a/main.js
+++ /dev/null
@@ -1,1217 +0,0 @@
-/*
-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 = ``;
-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 = [""];
- 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("");
- 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;
- }
-};