From adc34d31f11467cb9d253155f6b2c8db2bb93a92 Mon Sep 17 00:00:00 2001 From: Mateusz Tymek Date: Sat, 14 Feb 2026 16:57:42 +0100 Subject: [PATCH] Improved process management --- .../specs/process-launch/spec.md | 27 ++++ .../tasks.md | 91 ++++++------ src/main.ts | 37 +++++ src/server/ExecutableResolver.ts | 133 ++++++++++++++++++ src/server/ServerManager.ts | 83 +++++++---- src/server/process/PosixProcess.ts | 11 +- src/server/process/WindowsProcess.ts | 2 +- src/settings/SettingsTab.ts | 82 +++++++++-- src/types.ts | 4 + styles.css | 15 ++ 10 files changed, 407 insertions(+), 78 deletions(-) create mode 100644 src/server/ExecutableResolver.ts diff --git a/openspec/changes/improved-opencode-process-management/specs/process-launch/spec.md b/openspec/changes/improved-opencode-process-management/specs/process-launch/spec.md index a3cf41e..eabc440 100644 --- a/openspec/changes/improved-opencode-process-management/specs/process-launch/spec.md +++ b/openspec/changes/improved-opencode-process-management/specs/process-launch/spec.md @@ -67,3 +67,30 @@ The system SHALL maintain backward compatibility with existing opencodePath conf - **WHEN** the plugin loads - **THEN** the system SHALL use the existing opencodePath - **AND** continue working in path mode with default arguments + +### Requirement: Detailed error messages +The system SHALL provide clear, actionable error messages when the server fails to start. + +#### Scenario: Executable not found +- **GIVEN** useCustomCommand is false +- **AND** opencodePath is set to a non-existent absolute path +- **WHEN** the server fails to start +- **THEN** the error message SHALL be: "Executable not found at ''. Check Settings → OpenCode path, or click 'Autodetect'" + +#### Scenario: Executable exists but not executable +- **GIVEN** useCustomCommand is false +- **AND** opencodePath points to a file that exists but lacks execute permission +- **WHEN** the server fails to start +- **THEN** the error message SHALL be: "'' exists but is not executable. Run: chmod +x " + +#### Scenario: Error displayed in Settings UI +- **GIVEN** the server state is "error" +- **AND** an error message is stored +- **WHEN** the user views the Settings page +- **THEN** the error message SHALL be displayed below the "Status: Error" badge + +#### Scenario: Error displayed as toast notification +- **GIVEN** the user attempts to start the server from the main UI (not Settings) +- **AND** the server fails to start +- **THEN** a toast notification SHALL display the error message +- **AND** the toast SHALL remain visible for 10 seconds diff --git a/openspec/changes/improved-opencode-process-management/tasks.md b/openspec/changes/improved-opencode-process-management/tasks.md index c562ed8..2ea7064 100644 --- a/openspec/changes/improved-opencode-process-management/tasks.md +++ b/openspec/changes/improved-opencode-process-management/tasks.md @@ -1,68 +1,77 @@ ## 1. Settings Schema Updates -- [ ] 1.1 Add `customCommand` field to `OpenCodeSettings` interface in `src/types.ts` -- [ ] 1.2 Add `useCustomCommand` boolean field to `OpenCodeSettings` interface -- [ ] 1.3 Update `DEFAULT_SETTINGS` with new fields (empty string and false) +- [x] 1.1 Add `customCommand` field to `OpenCodeSettings` interface in `src/types.ts` +- [x] 1.2 Add `useCustomCommand` boolean field to `OpenCodeSettings` interface +- [x] 1.3 Update `DEFAULT_SETTINGS` with new fields (empty string and false) ## 2. Executable Resolver Module -- [ ] 2.1 Create `src/server/ExecutableResolver.ts` with cross-platform detection logic -- [ ] 2.2 Implement `resolveFromPath()` to check PATH for 'opencode' executable -- [ ] 2.3 Implement `resolve()` method with proper precedence: +- [x] 2.1 Create `src/server/ExecutableResolver.ts` with cross-platform detection logic +- [x] 2.2 Implement `resolveFromPath()` to check PATH for 'opencode' executable +- [x] 2.3 Implement `resolve()` method with proper precedence: - Check if configured path is absolute and exists → return it - Extract basename from configured path (handle both "opencode" and "/path/to/opencode") -- [ ] 2.4 Implement platform-specific directory search: +- [x] 2.4 Implement platform-specific directory search: - Linux: ~/.local/bin/, ~/.opencode/bin/, ~/.bun/bin/, ~/.npm-global/bin/, ~/.nvm/versions/node/*/bin/, /usr/local/bin/, /usr/bin/ - macOS: ~/.local/bin/, /opt/homebrew/bin/, /usr/local/bin/ - Windows: %LOCALAPPDATA%\opencode\bin\, %USERPROFILE%\.bun\bin\, %USERPROFILE%\.local\bin\ -- [ ] 2.5 Implement nvm wildcard expansion for ~/.nvm/versions/node/*/bin/ -- [ ] 2.6 Ensure fallback: return configured path if search fails -- [ ] 2.4 Implement main `resolve()` method that tries PATH first, then platform locations -- [ ] 2.5 Add helper to expand Windows environment variables (%LOCALAPPDATA%, %USERPROFILE%) +- [x] 2.5 Implement nvm wildcard expansion for ~/.nvm/versions/node/*/bin/ +- [x] 2.6 Ensure fallback: return configured path if search fails ## 3. ServerManager Updates -- [ ] 3.1 Modify `ServerManager.start()` to check `useCustomCommand` setting -- [ ] 3.2 Implement path mode spawning (direct spawn with default args) -- [ ] 3.3 Implement custom command mode spawning (shell: true, no args appended) -- [ ] 3.4 Ensure working directory is set correctly for both modes -- [ ] 3.5 Add support for verifying path mode executable with `opencode --version` +- [x] 3.1 Modify `ServerManager.start()` to check `useCustomCommand` setting +- [x] 3.2 Implement path mode spawning (direct spawn with default args) +- [x] 3.3 Implement custom command mode spawning (shell: true, no args appended) +- [x] 3.4 Ensure working directory is set correctly for both modes +- [x] 3.5 Add support for verifying path mode executable with `opencode --version` ## 4. Main Plugin Integration -- [ ] 4.1 Add autodetect logic in `main.ts` `onload()` method -- [ ] 4.2 Implement autodetect trigger: when `opencodePath` is empty and `useCustomCommand` is false -- [ ] 4.3 On successful autodetect: save path to settings, show success Notice -- [ ] 4.4 On failed autodetect: show error Notice "Could not find opencode. Please check Settings" -- [ ] 4.5 Import and use `ExecutableResolver` from main plugin +- [x] 4.1 Add autodetect logic in `main.ts` `onload()` method +- [x] 4.2 Implement autodetect trigger: when `opencodePath` is empty and `useCustomCommand` is false +- [x] 4.3 On successful autodetect: save path to settings, show success Notice +- [x] 4.4 On failed autodetect: show error Notice "Could not find opencode. Please check Settings" +- [x] 4.5 Import and use `ExecutableResolver` from main plugin ## 5. Settings UI Updates -- [ ] 5.1 Add toggle switch "Use custom command" in `SettingsTab.ts` -- [ ] 5.2 Conditionally show path input field when toggle is off -- [ ] 5.3 Conditionally show custom command textarea when toggle is on -- [ ] 5.4 Add "Autodetect" button next to path input -- [ ] 5.5 Implement autodetect button click handler that: +- [x] 5.1 Add toggle switch "Use custom command" in `SettingsTab.ts` +- [x] 5.2 Conditionally show path input field when toggle is off +- [x] 5.3 Conditionally show custom command textarea when toggle is on +- [x] 5.4 Add "Autodetect" button next to path input +- [x] 5.5 Implement autodetect button click handler that: - Calls `ExecutableResolver.resolve()` - Updates path input if found - Shows success/error Notice -- [ ] 5.6 Add descriptive text explaining custom command mode behavior -- [ ] 5.7 Ensure settings are saved when toggling between modes +- [x] 5.6 Add descriptive text explaining custom command mode behavior +- [x] 5.7 Ensure settings are saved when toggling between modes ## 6. Testing & Validation -- [ ] 6.1 Test autodetect finds opencode in PATH -- [ ] 6.2 Test autodetect finds opencode in platform-specific location -- [ ] 6.3 Test autodetect shows error when not found -- [ ] 6.4 Test path mode spawns correctly with default args -- [ ] 6.5 Test custom command mode spawns via shell without extra args -- [ ] 6.6 Test custom command with environment variables works -- [ ] 6.7 Verify backward compatibility: existing opencodePath values still work -- [ ] 6.8 Test manual autodetect button in Settings -- [ ] 6.9 Test toggle saves and restores correctly +- [x] 6.1 Test autodetect finds opencode in PATH +- [x] 6.2 Test autodetect finds opencode in platform-specific location +- [x] 6.3 Test autodetect shows error when not found +- [x] 6.4 Test path mode spawns correctly with default args +- [x] 6.5 Test custom command mode spawns via shell without extra args +- [x] 6.6 Test custom command with environment variables works +- [x] 6.7 Verify backward compatibility: existing opencodePath values still work +- [x] 6.8 Test manual autodetect button in Settings +- [x] 6.9 Test toggle saves and restores correctly -## 7. Documentation +## 7. Error Message Improvements -- [ ] 7.1 Update README.md with new settings options -- [ ] 7.2 Document autodetect behavior and common installation locations -- [ ] 7.3 Add examples of custom command usage +- [x] 7.1 Enhance verifyCommand in PosixProcess.ts with detailed error messages + - Distinguish between "not found" and "not executable" + - Provide chmod instructions for permission errors + - Include actionable guidance ("Check Settings → OpenCode path, or click 'Autodetect'") +- [x] 7.2 Enhance verifyCommand in WindowsProcess.ts with detailed error messages +- [x] 7.3 Display error details in Settings UI below "Status: Error" badge +- [x] 7.4 Show toast notification with error message when activation fails from main UI +- [x] 7.5 Add CSS styling for error message display in settings + +## 8. Documentation + +- [x] 8.1 Update README.md with new settings options +- [x] 8.2 Document autodetect behavior and common installation locations +- [x] 8.3 Add examples of custom command usage diff --git a/src/main.ts b/src/main.ts index 561cfb8..6475111 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { ServerManager, ServerState } from "./server/ServerManager"; import { registerOpenCodeIcons, OPENCODE_ICON_NAME } from "./icons"; import { OpenCodeClient } from "./client/OpenCodeClient"; import { ContextManager } from "./context/ContextManager"; +import { ExecutableResolver } from "./server/ExecutableResolver"; export default class OpenCodePlugin extends Plugin { settings: OpenCodeSettings = DEFAULT_SETTINGS; @@ -25,6 +26,9 @@ export default class OpenCodePlugin extends Plugin { await this.loadSettings(); + // Attempt autodetect if opencodePath is empty and not using custom command + await this.attemptAutodetect(); + const projectDirectory = this.getProjectDirectory(); this.processManager = new ServerManager(this.settings, projectDirectory); @@ -153,6 +157,32 @@ export default class OpenCodePlugin extends Plugin { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } + /** + * Attempt to autodetect opencode executable on startup + * Triggers when opencodePath is empty and useCustomCommand is false + */ + private async attemptAutodetect(): Promise { + // Only autodetect if path is empty and not using custom command mode + if (this.settings.opencodePath || this.settings.useCustomCommand) { + return; + } + + console.log("[OpenCode] Attempting to autodetect opencode executable..."); + + const detectedPath = ExecutableResolver.resolve("opencode"); + + // Check if a different path was found (not the fallback) + if (detectedPath && detectedPath !== "opencode") { + console.log("[OpenCode] Autodetected opencode at:", detectedPath); + this.settings.opencodePath = detectedPath; + await this.saveData(this.settings); + new Notice(`OpenCode executable found at ${detectedPath}`); + } else { + console.log("[OpenCode] Could not autodetect opencode executable"); + new Notice("Could not find opencode. Please check Settings"); + } + } + async saveSettings(): Promise { await this.saveData(this.settings); this.processManager.updateSettings(this.settings); @@ -165,6 +195,13 @@ export default class OpenCodePlugin extends Plugin { const success = await this.processManager.start(); if (success) { new Notice("OpenCode server started"); + } else { + const error = this.processManager.getLastError(); + if (error) { + new Notice(`OpenCode failed to start: ${error}`, 10000); // Show for 10 seconds + } else { + new Notice("OpenCode failed to start. Check Settings for details.", 5000); + } } return success; } diff --git a/src/server/ExecutableResolver.ts b/src/server/ExecutableResolver.ts new file mode 100644 index 0000000..b0c13ab --- /dev/null +++ b/src/server/ExecutableResolver.ts @@ -0,0 +1,133 @@ +import { existsSync } from "fs"; +import { homedir, platform } from "os"; +import { join, basename, isAbsolute } from "path"; +import { execSync } from "child_process"; + +/** + * Resolves the opencode executable path across different platforms. + * Follows the search algorithm: + * 1. If configured path is absolute and exists, return it directly + * 2. Extract basename from configured path + * 3. Search platform-specific locations for that basename + * 4. If found, return full path; if not found, return configured path as fallback + */ +export class ExecutableResolver { + /** + * Resolve the executable path based on configuration and platform + * @param configuredPath The path configured in settings (e.g., "opencode" or "/path/to/opencode") + * @returns The resolved full path or the configured path as fallback + */ + static resolve(configuredPath: string): string { + // If configured path is absolute and exists, use it directly + if (isAbsolute(configuredPath) && existsSync(configuredPath)) { + return configuredPath; + } + + // Extract basename (e.g., "opencode" from "/path/to/opencode" or just "opencode") + const execName = basename(configuredPath) || configuredPath; + + // Get search directories for current platform + const searchDirs = this.getSearchDirectories(); + + // Search for executable in platform directories + for (const dir of searchDirs) { + const fullPath = join(dir, execName); + if (existsSync(fullPath)) { + console.log("[OpenCode] Found executable at:", fullPath); + return fullPath; + } + } + + // Fallback: return configured path (let spawn fail naturally if not found) + console.log("[OpenCode] Executable not found in common paths, using configured:", configuredPath); + return configuredPath; + } + + /** + * Check if executable exists in PATH + * @param execName Name of executable to search for + * @returns Full path if found in PATH, null otherwise + */ + static resolveFromPath(execName: string): string | null { + try { + // Use 'which' on Unix systems, 'where' on Windows + const command = platform() === "win32" ? "where" : "which"; + const result = execSync(`${command} "${execName}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }); + const path = result.trim().split("\n")[0]; + if (path && existsSync(path)) { + return path; + } + } catch { + // Command not found in PATH + } + return null; + } + + /** + * Get platform-specific directories to search for executables + */ + private static getSearchDirectories(): string[] { + const currentPlatform = platform(); + const homeDir = homedir(); + const searchDirs: string[] = []; + + if (currentPlatform === "linux" || currentPlatform === "darwin") { + // User directories + searchDirs.push( + join(homeDir, ".local", "bin"), + join(homeDir, ".opencode", "bin"), + join(homeDir, ".bun", "bin"), + join(homeDir, ".npm-global", "bin") + ); + + // nvm directories (expand wildcard) + const nvmDirs = this.expandNvmDirectories(homeDir); + searchDirs.push(...nvmDirs); + + // System directories + searchDirs.push("/usr/local/bin", "/usr/bin"); + + // macOS-specific directories + if (currentPlatform === "darwin") { + searchDirs.push("/opt/homebrew/bin"); + } + } else if (currentPlatform === "win32") { + // Windows directories with environment variable expansion + const localAppData = process.env.LOCALAPPDATA || join(homeDir, "AppData", "Local"); + const userProfile = process.env.USERPROFILE || homeDir; + + searchDirs.push( + join(localAppData, "opencode", "bin"), + join(userProfile, ".bun", "bin"), + join(userProfile, ".local", "bin") + ); + } + + return searchDirs; + } + + /** + * Expand nvm wildcard directories + * Searches ~/.nvm/versions/node/ for installed versions + */ + private static expandNvmDirectories(homeDir: string): string[] { + const nvmBaseDir = join(homeDir, ".nvm", "versions", "node"); + const nvmDirs: string[] = []; + + try { + if (existsSync(nvmBaseDir)) { + const { readdirSync } = require("fs"); + const versions = readdirSync(nvmBaseDir, { withFileTypes: true }); + for (const version of versions) { + if (version.isDirectory()) { + nvmDirs.push(join(nvmBaseDir, version.name, "bin")); + } + } + } + } catch { + // nvm directory doesn't exist or is not accessible + } + + return nvmDirs; + } +} diff --git a/src/server/ServerManager.ts b/src/server/ServerManager.ts index 1d6a35d..f4aa368 100644 --- a/src/server/ServerManager.ts +++ b/src/server/ServerManager.ts @@ -1,10 +1,11 @@ -import { ChildProcess } from "child_process"; +import { ChildProcess, SpawnOptions } from "child_process"; import { EventEmitter } from "events"; import { OpenCodeSettings } from "../types"; import { ServerState } from "./types"; import { OpenCodeProcess } from "./process/OpenCodeProcess"; import { WindowsProcess } from "./process/WindowsProcess"; import { PosixProcess } from "./process/PosixProcess"; +import { ExecutableResolver } from "./ExecutableResolver"; export type { ServerState } from "./types"; @@ -60,10 +61,34 @@ export class ServerManager extends EventEmitter { return this.setError("Project directory (vault) not configured"); } - // Pre-flight check: verify executable exists - const commandError = await this.processImpl.verifyCommand(this.settings.opencodePath); - if (commandError) { - return this.setError(commandError); + // Determine execution mode and resolve executable path + let executablePath: string; + let spawnOptions: SpawnOptions; + + if (this.settings.useCustomCommand) { + // Custom command mode: use custom command directly with shell + executablePath = this.settings.customCommand; + spawnOptions = { + cwd: this.projectDirectory, + env: { ...process.env, NODE_USE_SYSTEM_CA: "1" }, + stdio: ["ignore", "pipe", "pipe"], + shell: true, + }; + } else { + // Path mode: resolve executable and verify + executablePath = ExecutableResolver.resolve(this.settings.opencodePath); + + // Pre-flight check: verify executable exists (only for path mode) + const commandError = await this.processImpl.verifyCommand(executablePath); + if (commandError) { + return this.setError(commandError); + } + + spawnOptions = { + cwd: this.projectDirectory, + env: { ...process.env, NODE_USE_SYSTEM_CA: "1" }, + stdio: ["ignore", "pipe", "pipe"], + }; } if (await this.checkServerHealth()) { @@ -76,30 +101,37 @@ export class ServerManager extends EventEmitter { } console.log("[OpenCode] Starting server:", { - opencodePath: this.settings.opencodePath, + mode: this.settings.useCustomCommand ? "custom" : "path", + command: executablePath, port: this.settings.port, hostname: this.settings.hostname, cwd: this.projectDirectory, projectDirectory: this.projectDirectory, }); - this.process = this.processImpl.start( - 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"], - } - ); + if (this.settings.useCustomCommand) { + // Custom command mode: spawn with shell, no args appended + this.process = this.processImpl.start( + executablePath, + [], // User controls all arguments in custom command + spawnOptions + ); + } else { + // Path mode: spawn with default arguments + this.process = this.processImpl.start( + executablePath, + [ + "serve", + "--port", + this.settings.port.toString(), + "--hostname", + this.settings.hostname, + "--cors", + "app://obsidian.md", + ], + spawnOptions + ); + } console.log("[OpenCode] Process spawned with PID:", this.process.pid); @@ -131,8 +163,11 @@ export class ServerManager extends EventEmitter { this.process = null; if (err.code === "ENOENT") { + const command = this.settings.useCustomCommand + ? this.settings.customCommand + : this.settings.opencodePath; this.setError( - `Executable not found at '${this.settings.opencodePath}'` + `Executable not found: '${command}'` ); } else { this.setError(`Failed to start: ${err.message}`); diff --git a/src/server/process/PosixProcess.ts b/src/server/process/PosixProcess.ts index 0a6c4f5..56c308f 100644 --- a/src/server/process/PosixProcess.ts +++ b/src/server/process/PosixProcess.ts @@ -1,4 +1,5 @@ import { ChildProcess, spawn, SpawnOptions } from "child_process"; +import { existsSync } from "fs"; import { OpenCodeProcess } from "./OpenCodeProcess"; export class PosixProcess implements OpenCodeProcess { @@ -46,12 +47,16 @@ export class PosixProcess implements OpenCodeProcess { async verifyCommand(command: string): Promise { // Check if command is absolute path - verify it exists and is executable if (command.startsWith('/') || command.startsWith('./')) { + const fs = require('fs'); try { - const fs = require('fs'); fs.accessSync(command, fs.constants.X_OK); return null; - } catch { - return `Executable not found at '${command}'`; + } catch (err: any) { + // Check if file exists but isn't executable + if (existsSync(command)) { + return `'${command}' exists but is not executable. Run: chmod +x ${command}`; + } + return `Executable not found at '${command}'. Check Settings → OpenCode path, or click "Autodetect"`; } } // For non-absolute paths, let spawn handle it (will fire ENOENT if not found) diff --git a/src/server/process/WindowsProcess.ts b/src/server/process/WindowsProcess.ts index 6eefddd..f905500 100644 --- a/src/server/process/WindowsProcess.ts +++ b/src/server/process/WindowsProcess.ts @@ -35,7 +35,7 @@ export class WindowsProcess implements OpenCodeProcess { await this.execAsync(`where "${command}"`); return null; } catch { - return `Executable not found at '${command}'`; + return `Executable not found at '${command}'. Check Settings → OpenCode path, or click "Autodetect"`; } } diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index ac47c3c..02ca474 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -3,6 +3,7 @@ import { existsSync, statSync } from "fs"; import { homedir } from "os"; import { OpenCodeSettings, ViewLocation } from "../types"; import { ServerManager } from "../server/ServerManager"; +import { ExecutableResolver } from "../server/ExecutableResolver"; function expandTilde(path: string): string { if (path === "~") { @@ -62,21 +63,72 @@ export class OpenCodeSettingTab extends PluginSettingTab { }) ); + // Command Mode Toggle new 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.settings.opencodePath) + .setName("Use custom command") + .setDesc("Enable to use a custom shell command instead of the executable path") + .addToggle((toggle) => + toggle + .setValue(this.settings.useCustomCommand) .onChange(async (value) => { - this.settings.opencodePath = value || "opencode"; + this.settings.useCustomCommand = value; await this.onSettingsChange(); + // Re-render to show/hide appropriate fields + this.display(); }) ); + if (this.settings.useCustomCommand) { + // Custom Command Mode + new Setting(containerEl) + .setName("Custom command") + .setDesc("Full shell command to start OpenCode. You control all arguments (e.g., 'opencode serve --port 14096')") + .addTextArea((text) => { + text + .setPlaceholder("opencode serve --port 14096 --hostname 127.0.0.1") + .setValue(this.settings.customCommand) + .onChange(async (value) => { + this.settings.customCommand = value; + await this.onSettingsChange(); + }); + text.inputEl.rows = 3; + text.inputEl.style.width = "100%"; + return text; + }); + } else { + // Path Mode + const pathSetting = new Setting(containerEl) + .setName("OpenCode path") + .setDesc("Path to the OpenCode executable. Leave empty to autodetect.") + .addText((text) => + text + .setPlaceholder("opencode") + .setValue(this.settings.opencodePath) + .onChange(async (value) => { + this.settings.opencodePath = value; + await this.onSettingsChange(); + }) + ); + + // Add Autodetect button + pathSetting.addButton((button) => { + button + .setButtonText("Autodetect") + .onClick(async () => { + const detectedPath = ExecutableResolver.resolve("opencode"); + if (detectedPath && detectedPath !== "opencode") { + this.settings.opencodePath = detectedPath; + await this.onSettingsChange(); + // Refresh the text input + this.display(); + new Notice(`OpenCode executable found at ${detectedPath}`); + } else { + new Notice("Could not find opencode. Please check your installation."); + } + }); + }); + } + new Setting(containerEl) .setName("Project directory") .setDesc( @@ -241,6 +293,18 @@ export class OpenCodeSettingTab extends PluginSettingTab { cls: `opencode-status-badge ${statusClass[state]}`, }); + // Show error message if state is error + if (state === "error") { + const errorMsg = this.serverManager.getLastError(); + if (errorMsg) { + const errorEl = container.createDiv({ cls: "opencode-error-details" }); + errorEl.createEl("div", { + text: errorMsg, + cls: "opencode-error-text" + }); + } + } + if (state === "running") { const urlEl = container.createDiv({ cls: "opencode-status-line" }); urlEl.createSpan({ text: "URL: " }); diff --git a/src/types.ts b/src/types.ts index deddf08..1f95282 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,8 @@ export interface OpenCodeSettings { injectWorkspaceContext: boolean; maxNotesInContext: number; maxSelectionLength: number; + customCommand: string; + useCustomCommand: boolean; } export const DEFAULT_SETTINGS: OpenCodeSettings = { @@ -24,6 +26,8 @@ export const DEFAULT_SETTINGS: OpenCodeSettings = { injectWorkspaceContext: false, maxNotesInContext: 20, maxSelectionLength: 2000, + customCommand: "", + useCustomCommand: false, }; export const OPENCODE_VIEW_TYPE = "opencode-view"; diff --git a/styles.css b/styles.css index 0358049..2bce793 100644 --- a/styles.css +++ b/styles.css @@ -191,3 +191,18 @@ color: var(--text-muted); font-style: italic; } + +/* Error details in settings */ +.opencode-error-details { + margin-top: 12px; + padding: 10px 12px; + background-color: var(--background-secondary); + border-radius: 6px; + border-left: 3px solid var(--text-error); +} + +.opencode-error-text { + color: var(--text-error); + font-size: 0.9em; + line-height: 1.4; +}