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,
|
cli,
|
||||||
llm,
|
llm,
|
||||||
voice,
|
voice,
|
||||||
|
inference,
|
||||||
)
|
)
|
||||||
from livekit.agents.llm.mcp import MCPServerHTTP
|
from livekit.agents.llm.mcp import MCPServerHTTP
|
||||||
|
|
||||||
@@ -67,7 +68,11 @@ async def entrypoint(ctx: JobContext):
|
|||||||
session = voice.AgentSession(
|
session = voice.AgentSession(
|
||||||
stt="assemblyai/universal-streaming:en",
|
stt="assemblyai/universal-streaming:en",
|
||||||
llm="openai/gpt-4.1-mini",
|
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
|
# Start the session
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import * as tmux from "./tmux.js";
|
|||||||
import { startHttpServer } from "./http-server.js";
|
import { startHttpServer } from "./http-server.js";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
const DEFAULT_SESSION = "voice-dev";
|
const DEFAULT_SESSION = "__voice-dev";
|
||||||
|
|
||||||
// Create MCP server
|
// Create MCP server
|
||||||
const server = new McpServer(
|
const server = new McpServer(
|
||||||
@@ -47,7 +47,7 @@ async function ensureDefaultSession(): Promise<void> {
|
|||||||
// List terminals - Tool
|
// List terminals - Tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"list-terminals",
|
"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 () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +61,7 @@ server.tool(
|
|||||||
|
|
||||||
const windows = await tmux.listWindows(session.id);
|
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(
|
const terminals = await Promise.all(
|
||||||
windows.map(async (window) => {
|
windows.map(async (window) => {
|
||||||
const panes = await tmux.listPanes(window.id);
|
const panes = await tmux.listPanes(window.id);
|
||||||
@@ -69,13 +69,16 @@ server.tool(
|
|||||||
const workingDirectory = pane
|
const workingDirectory = pane
|
||||||
? await tmux.getCurrentWorkingDirectory(pane.id)
|
? await tmux.getCurrentWorkingDirectory(pane.id)
|
||||||
: "unknown";
|
: "unknown";
|
||||||
|
const currentCommand = pane
|
||||||
|
? await tmux.getCurrentCommand(pane.id)
|
||||||
|
: "unknown";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: window.id,
|
id: window.id,
|
||||||
name: window.name,
|
name: window.name,
|
||||||
active: window.active,
|
active: window.active,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
paneId: pane?.id || "",
|
currentCommand,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -130,8 +133,11 @@ server.tool(
|
|||||||
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
throw new Error(`Default session not found: ${DEFAULT_SESSION}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create window in default session
|
// Create window in default session with working directory and optional command
|
||||||
const window = await tmux.createWindow(session.id, name);
|
const window = await tmux.createWindow(session.id, name, {
|
||||||
|
workingDirectory,
|
||||||
|
command: initialCommand,
|
||||||
|
});
|
||||||
if (!window) {
|
if (!window) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -143,38 +149,13 @@ server.tool(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change to working directory
|
const commandOutput = window.output;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = `Terminal created: ${JSON.stringify(
|
let text = `Terminal created: ${JSON.stringify(
|
||||||
{
|
{
|
||||||
id: window.id,
|
id: window.id,
|
||||||
name: window.name,
|
name: window.name,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
paneId: window.paneId,
|
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
@@ -525,12 +506,16 @@ server.resource("Terminals", "tmux://terminals", async () => {
|
|||||||
const workingDirectory = pane
|
const workingDirectory = pane
|
||||||
? await tmux.getCurrentWorkingDirectory(pane.id)
|
? await tmux.getCurrentWorkingDirectory(pane.id)
|
||||||
: "unknown";
|
: "unknown";
|
||||||
|
const currentCommand = pane
|
||||||
|
? await tmux.getCurrentCommand(pane.id)
|
||||||
|
: "unknown";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: window.id,
|
id: window.id,
|
||||||
name: window.name,
|
name: window.name,
|
||||||
active: window.active,
|
active: window.active,
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
|
currentCommand,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -175,31 +175,94 @@ export async function capturePaneContent(
|
|||||||
export async function getCurrentWorkingDirectory(
|
export async function getCurrentWorkingDirectory(
|
||||||
paneId: string
|
paneId: string
|
||||||
): Promise<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> {
|
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);
|
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(
|
export async function createWindow(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
name: string,
|
name: string,
|
||||||
command?: string
|
options?: {
|
||||||
|
workingDirectory?: string;
|
||||||
|
command?: string;
|
||||||
|
}
|
||||||
): Promise<(TmuxWindow & { paneId: string; output?: string }) | null> {
|
): 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 windows = await listWindows(sessionId);
|
||||||
const window = windows.find((window) => window.name === name);
|
const window = windows.find((window) => window.name === name);
|
||||||
|
|
||||||
if (!window) return null;
|
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
|
// Get the default pane created with the window
|
||||||
const panes = await listPanes(window.id);
|
const panes = await listPanes(window.id);
|
||||||
const defaultPane = panes[0];
|
const defaultPane = panes[0];
|
||||||
@@ -207,10 +270,10 @@ export async function createWindow(
|
|||||||
let commandOutput: string | undefined;
|
let commandOutput: string | undefined;
|
||||||
|
|
||||||
// If command is provided, execute it in the new pane
|
// If command is provided, execute it in the new pane
|
||||||
if (command && defaultPane) {
|
if (options?.command && defaultPane) {
|
||||||
await sendText({
|
await sendText({
|
||||||
paneId: defaultPane.id,
|
paneId: defaultPane.id,
|
||||||
text: command,
|
text: options.command,
|
||||||
pressEnter: true,
|
pressEnter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,6 +320,8 @@ export async function renameWindow(
|
|||||||
name: string
|
name: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await executeTmux(`rename-window -t '${windowId}' '${name}'`);
|
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