From 55465575aabe34c8b03ecc7265b80a0de03d50d1 Mon Sep 17 00:00:00 2001 From: Chris Lettieri Date: Tue, 24 Feb 2026 21:14:26 -0500 Subject: [PATCH] tmux detect --- src/services/task-dispatch-service.ts | 77 +++++++++++++++++++++------ src/types/settings.ts | 1 + src/types/task-dispatch.ts | 1 + src/ui/settings-tab.ts | 32 +++++++++++ 4 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/services/task-dispatch-service.ts b/src/services/task-dispatch-service.ts index 19eec63..d1cdf13 100644 --- a/src/services/task-dispatch-service.ts +++ b/src/services/task-dispatch-service.ts @@ -9,6 +9,38 @@ import { AgentConfig, TaskSession } from '../types/task-dispatch'; const execAsync = promisify(exec); +/** Common locations where Homebrew installs tmux. */ +const TMUX_SEARCH_PATHS = [ + '/opt/homebrew/bin/tmux', // Apple Silicon Homebrew + '/usr/local/bin/tmux', // Intel Homebrew + '/usr/bin/tmux', // system install +]; + +/** + * Try to locate the tmux binary on disk. + * Returns the absolute path if found, or null. + */ +export async function detectTmuxPath(): Promise { + for (const p of TMUX_SEARCH_PATHS) { + try { + await fs.promises.access(p, fs.constants.X_OK); + return p; + } catch { /* not here */ } + } + // Fallback: try `which` with an augmented PATH + try { + const { stdout } = await execAsync('which tmux', { + env: { + ...process.env, + PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH ?? '/usr/bin:/bin'}`, + }, + }); + const found = stdout.trim(); + if (found) return found; + } catch { /* not found */ } + return null; +} + export class TaskDispatchService { private app: App; private settings: OpenAugiSettings; @@ -24,6 +56,16 @@ export class TaskDispatchService { this.distillService = distillService; } + /** Resolve the tmux binary path from settings or auto-detect. */ + private async getTmux(): Promise { + const configured = this.settings.taskDispatch.tmuxPath; + if (configured) return configured; + + const detected = await detectTmuxPath(); + if (detected) return detected; + throw new Error('tmux not found. Set the path in Settings → Task Dispatch, or install with: brew install tmux'); + } + /** * Launch a new session or attach to an existing one for the given task note. * The plugin only reads task_id from frontmatter — all other task state @@ -40,7 +82,8 @@ export class TaskDispatchService { const agentConfig = this.getAgentConfig(); try { - const exists = await this.tmuxSessionExists(sessionName); + const tmux = await this.getTmux(); + const exists = await this.tmuxSessionExists(tmux, sessionName); if (exists) { new Notice(`Attaching to session: ${taskId}`); @@ -51,18 +94,13 @@ export class TaskDispatchService { const contextContent = await this.assembleContext(file, taskId); const contextFilePath = await this.writeContextFile(taskId, contextContent); - await this.createTmuxSession(sessionName, agentConfig, contextFilePath); + await this.createTmuxSession(tmux, sessionName, agentConfig, contextFilePath); await this.openTerminal(sessionName); } } catch (error) { console.error('Task dispatch error:', error); const msg = error instanceof Error ? error.message : String(error); - - if (msg.includes('tmux') && msg.includes('not found')) { - new Notice('tmux is not installed. Install it with: brew install tmux'); - } else { - new Notice(`Task dispatch failed: ${msg}`); - } + new Notice(`Task dispatch failed: ${msg}`); } } @@ -79,13 +117,14 @@ export class TaskDispatchService { const sessionName = `task-${taskId}`; try { - const exists = await this.tmuxSessionExists(sessionName); + const tmux = await this.getTmux(); + const exists = await this.tmuxSessionExists(tmux, sessionName); if (!exists) { new Notice(`No active session for: ${taskId}`); return; } - await execAsync(`tmux kill-session -t ${this.shellEscape(sessionName)}`); + await execAsync(`${tmux} kill-session -t ${this.shellEscape(sessionName)}`); this.cleanupContextFile(taskId); new Notice(`Killed session: ${taskId}`); @@ -101,7 +140,8 @@ export class TaskDispatchService { async killSessionById(taskId: string): Promise { const sessionName = `task-${taskId}`; try { - await execAsync(`tmux kill-session -t ${this.shellEscape(sessionName)}`); + const tmux = await this.getTmux(); + await execAsync(`${tmux} kill-session -t ${this.shellEscape(sessionName)}`); this.cleanupContextFile(taskId); } catch (error) { console.error('Failed to kill session:', error); @@ -114,8 +154,9 @@ export class TaskDispatchService { */ async listActiveSessions(): Promise { try { + const tmux = await this.getTmux(); const { stdout } = await execAsync( - 'tmux list-sessions -F "#{session_name} #{session_created}" 2>/dev/null' + `${tmux} list-sessions -F "#{session_name} #{session_created}" 2>/dev/null` ); const sessions: TaskSession[] = []; @@ -155,8 +196,9 @@ export class TaskDispatchService { * Open terminal attached to a tmux session (public for use from modal). */ async openTerminal(sessionName: string): Promise { + const tmux = await this.getTmux(); const terminalApp = this.settings.taskDispatch.terminalApp; - const attachCmd = `tmux attach -t ${this.shellEscape(sessionName)}`; + const attachCmd = `${tmux} attach -t ${this.shellEscape(sessionName)}`; let osascript: string; if (terminalApp === 'iterm2') { @@ -242,9 +284,9 @@ export class TaskDispatchService { } } - private async tmuxSessionExists(sessionName: string): Promise { + private async tmuxSessionExists(tmux: string, sessionName: string): Promise { try { - await execAsync(`tmux has-session -t ${this.shellEscape(sessionName)} 2>/dev/null`); + await execAsync(`${tmux} has-session -t ${this.shellEscape(sessionName)} 2>/dev/null`); return true; } catch { return false; @@ -252,15 +294,16 @@ export class TaskDispatchService { } private async createTmuxSession( + tmux: string, sessionName: string, agentConfig: AgentConfig, contextFilePath: string ): Promise { - await execAsync(`tmux new-session -d -s ${this.shellEscape(sessionName)}`); + await execAsync(`${tmux} new-session -d -s ${this.shellEscape(sessionName)}`); const agentCommand = `${agentConfig.command} ${agentConfig.contextFlag} ${this.shellEscape(contextFilePath)}`; await execAsync( - `tmux send-keys -t ${this.shellEscape(sessionName)} ${this.shellEscape(agentCommand)} Enter` + `${tmux} send-keys -t ${this.shellEscape(sessionName)} ${this.shellEscape(agentCommand)} Enter` ); } diff --git a/src/types/settings.ts b/src/types/settings.ts index a7328ca..2b139dc 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -58,6 +58,7 @@ export const DEFAULT_SETTINGS: OpenAugiSettings = { }, taskDispatch: { terminalApp: 'iterm2', + tmuxPath: '', agents: [ { id: 'claude-code', diff --git a/src/types/task-dispatch.ts b/src/types/task-dispatch.ts index 926dd07..cb57d57 100644 --- a/src/types/task-dispatch.ts +++ b/src/types/task-dispatch.ts @@ -11,6 +11,7 @@ export type TerminalApp = 'iterm2' | 'terminal-app'; export interface TaskDispatchSettings { terminalApp: TerminalApp; + tmuxPath: string; // absolute path to tmux binary; empty = auto-detect agents: AgentConfig[]; defaultAgent: string; contextTempDir: string; diff --git a/src/ui/settings-tab.ts b/src/ui/settings-tab.ts index 6bfb308..0bae61d 100644 --- a/src/ui/settings-tab.ts +++ b/src/ui/settings-tab.ts @@ -2,6 +2,7 @@ import { App, PluginSettingTab, Setting, Notice, DropdownComponent } from 'obsid import type OpenAugiPlugin from '../types/plugin'; import { OpenAIService } from '../services/openai-service'; import { TerminalApp } from '../types/task-dispatch'; +import { detectTmuxPath } from '../services/task-dispatch-service'; export class OpenAugiSettingTab extends PluginSettingTab { plugin: OpenAugiPlugin; @@ -367,6 +368,37 @@ export class OpenAugiSettingTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setName('tmux path') + .setDesc('Absolute path to the tmux binary. Leave empty to auto-detect.') + .addText(text => { + text + .setPlaceholder('Auto-detect') + .setValue(this.plugin.settings.taskDispatch.tmuxPath); + text.inputEl.addEventListener('blur', async () => { + const value = text.getValue().trim(); + if (value !== this.plugin.settings.taskDispatch.tmuxPath) { + this.plugin.settings.taskDispatch.tmuxPath = value; + await this.plugin.saveSettings(); + } + }); + return text; + }) + .addButton(button => button + .setButtonText('Detect') + .onClick(async () => { + const found = await detectTmuxPath(); + if (found) { + this.plugin.settings.taskDispatch.tmuxPath = found; + await this.plugin.saveSettings(); + new Notice(`tmux found: ${found}`); + this.display(); // refresh the UI to show the detected path + } else { + new Notice('tmux not found. Install with: brew install tmux'); + } + }) + ); + new Setting(containerEl) .setName('Default agent') .setDesc('Agent to use when task note does not specify one')