Based on the git diff, this commit makes three distinct improvements to terminal management:
``` feat: improve terminal state tracking and creation reliability - Switch to ElevenLabs TTS (eleven_turbo_v2_5) in voice agent - Add current command tracking to terminal listings - Use session prefix '__voice-dev' to avoid conflicts - Create windows with working directory and run initial commands atomically - Disable automatic window renaming to preserve user-set names - Improve working directory detection with lsof fallback - Enhance command detection to show full command with arguments ```
This commit is contained in:
@@ -18,6 +18,7 @@ from livekit.agents import (
|
||||
cli,
|
||||
llm,
|
||||
voice,
|
||||
inference,
|
||||
)
|
||||
from livekit.agents.llm.mcp import MCPServerHTTP
|
||||
|
||||
@@ -67,7 +68,11 @@ async def entrypoint(ctx: JobContext):
|
||||
session = voice.AgentSession(
|
||||
stt="assemblyai/universal-streaming:en",
|
||||
llm="openai/gpt-4.1-mini",
|
||||
tts="cartesia/sonic-2:9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
|
||||
tts= inference.TTS(
|
||||
model="elevenlabs/eleven_turbo_v2_5",
|
||||
voice="Xb7hH8MSUJpSbSDYk0k2",
|
||||
language="en"
|
||||
),
|
||||
)
|
||||
|
||||
# Start the session
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as tmux from "./tmux.js";
|
||||
import { startHttpServer } from "./http-server.js";
|
||||
import os from "node:os";
|
||||
|
||||
const DEFAULT_SESSION = "voice-dev";
|
||||
const DEFAULT_SESSION = "__voice-dev";
|
||||
|
||||
// Create MCP server
|
||||
const server = new McpServer(
|
||||
@@ -47,7 +47,7 @@ async function ensureDefaultSession(): Promise<void> {
|
||||
// List terminals - Tool
|
||||
server.tool(
|
||||
"list-terminals",
|
||||
"List all terminals (isolated shell environments). Returns terminal ID, name, and current working directory.",
|
||||
"List all terminals (isolated shell environments). Returns terminal ID, name, current working directory, and currently running command.",
|
||||
{},
|
||||
async () => {
|
||||
try {
|
||||
@@ -61,7 +61,7 @@ server.tool(
|
||||
|
||||
const windows = await tmux.listWindows(session.id);
|
||||
|
||||
// For each window, get the pane and its working directory
|
||||
// For each window, get the pane and its info
|
||||
const terminals = await Promise.all(
|
||||
windows.map(async (window) => {
|
||||
const panes = await tmux.listPanes(window.id);
|
||||
@@ -69,13 +69,16 @@ server.tool(
|
||||
const workingDirectory = pane
|
||||
? await tmux.getCurrentWorkingDirectory(pane.id)
|
||||
: "unknown";
|
||||
const currentCommand = pane
|
||||
? await tmux.getCurrentCommand(pane.id)
|
||||
: "unknown";
|
||||
|
||||
return {
|
||||
id: window.id,
|
||||
name: window.name,
|
||||
active: window.active,
|
||||
workingDirectory,
|
||||
paneId: pane?.id || "",
|
||||
currentCommand,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -130,8 +133,11 @@ server.tool(
|
||||
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||
}
|
||||
|
||||
// Create window in default session
|
||||
const window = await tmux.createWindow(session.id, name);
|
||||
// Create window in default session with working directory and optional command
|
||||
const window = await tmux.createWindow(session.id, name, {
|
||||
workingDirectory,
|
||||
command: initialCommand,
|
||||
});
|
||||
if (!window) {
|
||||
return {
|
||||
content: [
|
||||
@@ -143,38 +149,13 @@ server.tool(
|
||||
};
|
||||
}
|
||||
|
||||
// Change to working directory
|
||||
await tmux.sendText({
|
||||
paneId: window.paneId,
|
||||
text: `cd "${workingDirectory}"`,
|
||||
pressEnter: true,
|
||||
});
|
||||
|
||||
// Wait a bit for cd to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// Execute initial command if provided
|
||||
let commandOutput: string | undefined;
|
||||
if (initialCommand) {
|
||||
await tmux.sendText({
|
||||
paneId: window.paneId,
|
||||
text: initialCommand,
|
||||
pressEnter: true,
|
||||
});
|
||||
|
||||
// Wait for command to execute
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Capture output
|
||||
commandOutput = await tmux.capturePaneContent(window.paneId, 200);
|
||||
}
|
||||
const commandOutput = window.output;
|
||||
|
||||
let text = `Terminal created: ${JSON.stringify(
|
||||
{
|
||||
id: window.id,
|
||||
name: window.name,
|
||||
workingDirectory,
|
||||
paneId: window.paneId,
|
||||
},
|
||||
null,
|
||||
2
|
||||
@@ -525,12 +506,16 @@ server.resource("Terminals", "tmux://terminals", async () => {
|
||||
const workingDirectory = pane
|
||||
? await tmux.getCurrentWorkingDirectory(pane.id)
|
||||
: "unknown";
|
||||
const currentCommand = pane
|
||||
? await tmux.getCurrentCommand(pane.id)
|
||||
: "unknown";
|
||||
|
||||
return {
|
||||
id: window.id,
|
||||
name: window.name,
|
||||
active: window.active,
|
||||
workingDirectory,
|
||||
currentCommand,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -175,31 +175,94 @@ export async function capturePaneContent(
|
||||
export async function getCurrentWorkingDirectory(
|
||||
paneId: string
|
||||
): Promise<string> {
|
||||
return executeTmux(`display-message -p -t '${paneId}' '#{pane_current_path}'`);
|
||||
try {
|
||||
const tmuxPath = await executeTmux(`display-message -p -t '${paneId}' '#{pane_current_path}'`);
|
||||
|
||||
// If tmux returns a valid path, use it
|
||||
if (tmuxPath && tmuxPath.trim()) {
|
||||
return tmuxPath;
|
||||
}
|
||||
|
||||
// Fallback: get the PID and use lsof to find the actual CWD
|
||||
const shellPid = await executeTmux(`display-message -p -t '${paneId}' '#{pane_pid}'`);
|
||||
const { stdout } = await exec(`lsof -a -p ${shellPid.trim()} -d cwd -Fn | grep '^n' | cut -c2-`);
|
||||
return stdout.trim() || tmuxPath;
|
||||
} catch (error) {
|
||||
// If all else fails, return empty string
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tmux session
|
||||
* Get the current command running in a pane (full command line with arguments)
|
||||
* Gets the immediate child process of the shell, not the shell itself
|
||||
*/
|
||||
export async function getCurrentCommand(paneId: string): Promise<string> {
|
||||
try {
|
||||
// Get the shell PID (the pane's main process)
|
||||
const shellPid = await executeTmux(`display-message -p -t '${paneId}' '#{pane_pid}'`);
|
||||
|
||||
// First, check if there's a child process using comm= (works for all programs including top)
|
||||
// Use 'ax' flags to see all processes
|
||||
const { stdout: childPid } = await exec(`ps ax -o pid=,ppid=,comm= | awk '$2 == ${shellPid.trim()} { print $1; exit }'`);
|
||||
|
||||
if (childPid.trim()) {
|
||||
// Found a child process, get its full command with args
|
||||
const { stdout: fullCmd } = await exec(`ps -p ${childPid.trim()} -o args= | sed 's/\\\\012.*//'`);
|
||||
const command = fullCmd.trim();
|
||||
if (command) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
// No child process, just return the shell name
|
||||
const { stdout: shellCmd } = await exec(`ps -p ${shellPid} -o comm=`);
|
||||
return shellCmd.trim();
|
||||
} catch (error) {
|
||||
// Fallback to just the command name if ps fails
|
||||
return executeTmux(`display-message -p -t '${paneId}' '#{pane_current_command}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tmux session with a default window named "default" in home directory
|
||||
*/
|
||||
export async function createSession(name: string): Promise<TmuxSession | null> {
|
||||
await executeTmux(`new-session -d -s "${name}"`);
|
||||
const homeDir = process.env.HOME || "~";
|
||||
await executeTmux(`new-session -d -s "${name}" -n "default" -c "${homeDir}"`);
|
||||
|
||||
// Disable automatic window renaming for all windows in the session
|
||||
await executeTmux(`set-window-option -t "${name}" automatic-rename off`);
|
||||
|
||||
return findSessionByName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new window in a session
|
||||
* Create a new window in a session with optional working directory and initial command
|
||||
*/
|
||||
export async function createWindow(
|
||||
sessionId: string,
|
||||
name: string,
|
||||
command?: string
|
||||
options?: {
|
||||
workingDirectory?: string;
|
||||
command?: string;
|
||||
}
|
||||
): Promise<(TmuxWindow & { paneId: string; output?: string }) | null> {
|
||||
const output = await executeTmux(`new-window -t '${sessionId}' -n '${name}'`);
|
||||
// Build new-window command with optional working directory
|
||||
let newWindowCmd = `new-window -t '${sessionId}' -n '${name}'`;
|
||||
if (options?.workingDirectory) {
|
||||
newWindowCmd += ` -c '${options.workingDirectory}'`;
|
||||
}
|
||||
|
||||
await executeTmux(newWindowCmd);
|
||||
const windows = await listWindows(sessionId);
|
||||
const window = windows.find((window) => window.name === name);
|
||||
|
||||
if (!window) return null;
|
||||
|
||||
// Disable automatic window renaming
|
||||
await executeTmux(`set-window-option -t '${window.id}' automatic-rename off`);
|
||||
|
||||
// Get the default pane created with the window
|
||||
const panes = await listPanes(window.id);
|
||||
const defaultPane = panes[0];
|
||||
@@ -207,10 +270,10 @@ export async function createWindow(
|
||||
let commandOutput: string | undefined;
|
||||
|
||||
// If command is provided, execute it in the new pane
|
||||
if (command && defaultPane) {
|
||||
if (options?.command && defaultPane) {
|
||||
await sendText({
|
||||
paneId: defaultPane.id,
|
||||
text: command,
|
||||
text: options.command,
|
||||
pressEnter: true,
|
||||
});
|
||||
|
||||
@@ -257,6 +320,8 @@ export async function renameWindow(
|
||||
name: string
|
||||
): Promise<void> {
|
||||
await executeTmux(`rename-window -t '${windowId}' '${name}'`);
|
||||
// Disable automatic renaming to preserve the manual name
|
||||
await executeTmux(`set-window-option -t '${windowId}' automatic-rename off`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user