mirror of
https://github.com/bitsofchris/openaugi-obsidian-plugin.git
synced 2026-07-22 05:46:42 +00:00
tmux detect
This commit is contained in:
parent
ae9f90acad
commit
55465575aa
4 changed files with 94 additions and 17 deletions
|
|
@ -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<string | null> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<TaskSession[]> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
private async tmuxSessionExists(tmux: string, sessionName: string): Promise<boolean> {
|
||||
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<void> {
|
||||
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`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export const DEFAULT_SETTINGS: OpenAugiSettings = {
|
|||
},
|
||||
taskDispatch: {
|
||||
terminalApp: 'iterm2',
|
||||
tmuxPath: '',
|
||||
agents: [
|
||||
{
|
||||
id: 'claude-code',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue