This commit is contained in:
Timothy Jaeryang Baek
2026-09-18 11:30:15 -04:00
parent 9d98ffcddf
commit ca1eefe293
5 changed files with 95 additions and 9 deletions
+48 -2
View File
@@ -3043,7 +3043,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if direct_tool_servers:
for tool_server in direct_tool_servers:
system_prompt = tool_server.pop('system_prompt', None)
if tool_server.get('is_terminal') is True and not terminal_capability:
continue
system_prompt = tool_server.get('system_prompt')
if system_prompt:
form_data['messages'] = add_or_update_system_message(
system_prompt,
@@ -3051,7 +3053,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
append=True,
)
tool_specs = tool_server.pop('specs', [])
tool_specs = tool_server.get('specs', [])
for tool in tool_specs:
tools_dict[tool['name']] = {
@@ -3107,6 +3109,50 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if name not in tools_dict:
tools_dict[name] = tool_dict
# Only advertise user-shell tools when the originating browser has a connected shell.
shell_tools = {
name: tool
for name, tool in tools_dict.items()
if name in {'read_user_terminal', 'send_user_terminal_input'}
and (tool.get('type') == 'terminal' or tool.get('server', {}).get('is_terminal') is True)
}
selected = {
name
for name, tool in shell_tools.items()
if terminal_id
and (
tool.get('tool_id') == f'terminal:{terminal_id}'
or (tool.get('direct') and tool.get('server', {}).get('url') == terminal_id)
)
}
connected = False
if (
selected
and event_caller
and metadata.get('session_id')
and metadata.get('chat_id')
and not metadata.get('automation_id')
and not metadata.get('internal')
):
try:
state = await asyncio.wait_for(
event_caller(
{
'type': 'request:terminal:state',
'data': {'terminal_id': terminal_id, 'session_id': metadata['session_id']},
}
),
timeout=2,
)
connected = isinstance(state, dict) and state.get('connected') is True
except Exception:
# Old/disconnected browsers cannot confirm availability; other tools still work.
pass
for name in shell_tools:
if not connected or name not in selected:
tools_dict.pop(name)
if tools_dict:
# Always store resolved tools in metadata so downstream consumers
# (e.g. pipe functions) can access all tools including MCP and builtins.
+5 -1
View File
@@ -3579,7 +3579,11 @@
(server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id)
),
// Direct terminal servers — always included when enabled (not routed through selectedToolIds)
...($terminalServers ?? []).filter((t) => !t.id)
...(terminalEnabled
? ($terminalServers ?? [])
.filter((server) => !server.id)
.map((server) => ({ ...server, is_terminal: true }))
: [])
],
features: getFeatures(),
variables: {
+25 -6
View File
@@ -5,6 +5,7 @@
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
import { terminalRequest, type TerminalConnection } from '$lib/apis/terminal';
import { connectedUserTerminals } from '$lib/stores';
export let connection: TerminalConnection;
export let chatId: string | null = null;
@@ -22,6 +23,17 @@
let pingInterval: ReturnType<typeof setInterval>;
let destroyed = false;
let sessionId = '';
const terminalOwner = Symbol();
function disconnect() {
connectedUserTerminals.update((entries) => {
entries.delete(terminalOwner);
return entries;
});
connected = false;
connecting = false;
clearInterval(pingInterval);
}
export function write(output: string) {
if (!term || destroyed) return;
@@ -59,9 +71,14 @@
ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
if (destroyed) return;
ws?.send(
JSON.stringify({ type: 'auth', token: connection.key.trim(), chat_id: chatId ?? '' })
);
if (connection.selector && chatId) {
const shell = { terminalId: connection.selector, chatId };
connectedUserTerminals.update((entries) => entries.set(terminalOwner, shell));
}
connected = true;
connecting = false;
fit();
@@ -72,21 +89,23 @@
}, 25000);
};
ws.onmessage = (event) => {
if (destroyed) return;
if (event.data instanceof ArrayBuffer) term?.write(new Uint8Array(event.data));
else write(event.data);
};
ws.onclose = () => {
connected = false;
connecting = false;
clearInterval(pingInterval);
if (destroyed) return;
disconnect();
write('\r\n\x1b[90m[Connection closed]\x1b[0m\r\n');
};
ws.onerror = () => {
connecting = false;
if (destroyed) return;
disconnect();
write('\r\n\x1b[31m[Terminal connection failed]\x1b[0m\r\n');
};
} catch (error) {
connecting = false;
if (destroyed) return;
disconnect();
write(`\r\n\x1b[31m[${error}]\x1b[0m\r\n`);
}
}
@@ -133,7 +152,7 @@
onDestroy(() => {
destroyed = true;
clearInterval(pingInterval);
disconnect();
ws?.close();
resizeObserver?.disconnect();
term?.dispose();
+3
View File
@@ -146,6 +146,9 @@ export type FileNavOpenRequest = string | { path: string; page?: number | null }
export const showFileNavPath: Writable<FileNavOpenRequest | null> = writable(null);
export const showFileNavDir: Writable<string | null> = writable(null);
export const selectedTerminalId: Writable<string | null> = writable(null);
export const connectedUserTerminals = writable(
new Map<symbol, { terminalId: string; chatId: string }>()
);
export const artifactCode = writable(null);
export const artifactContents = writable(null);
+14
View File
@@ -31,6 +31,7 @@
channels,
channelId,
terminalServers,
connectedUserTerminals,
showControls,
showFileNavPath,
showFileNavDir,
@@ -556,6 +557,19 @@
};
const chatEventHandler = async (event, cb) => {
// Answer this session's availability check even when another chat is active.
if (
event?.data?.type === 'request:terminal:state' &&
event.data.data?.session_id === $socket?.id
) {
cb?.({
connected: [...$connectedUserTerminals.values()].some(
(shell) =>
shell.terminalId === event.data.data?.terminal_id && shell.chatId === event.chat_id
)
});
return;
}
const chat = $page.url.pathname.includes(`/c/${event.chat_id}`);
// Skip events from temporary chats that are not the current chat.