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:
Mohamed Boudra
2025-10-14 13:11:57 +02:00
parent 605e5c3e87
commit f3b8cc689d
3 changed files with 96 additions and 41 deletions
+6 -1
View File
@@ -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
+17 -32
View File
@@ -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,
};
})
);
+73 -8
View File
@@ -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`);
}
/**