fix: scope the terminal ports panel and port preview to the chat

With an orchestrator terminal set to Per chat, the Ports section of the file panel queried the user's shared workspace instead of the chat's. Servers started in the chat never showed up, previewing or opening a port went to the shared workspace, and every 5 second poll started the shared workspace container again if it had stopped.

The port preview loads in an iframe and "open in new tab" in a new window, and neither can send the chat id header the other file panel requests use. So for chat-scoped terminals the ports panel now talks to `/terminals/<id>/chats/<chat_id>/...`, and the terminal proxy reads the chat id from that path, going through the same saved-chat, access and path checks as the header. Relative asset requests from the previewed app keep the prefix, so the whole page stays in the chat's workspace, and switching chats refreshes the list right away.

A query parameter was the other option, but relative sub-requests drop it and it would be forwarded to the previewed app. Shared connections and personal terminals keep their current URLs.
This commit is contained in:
Classic298 2026-09-25 00:28:43 +02:00
parent fccd755684
commit e3b052ba4a
2 changed files with 11 additions and 3 deletions

View file

@ -107,6 +107,8 @@ PROXY_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
@router.api_route('/{server_id}/{path:path}', methods=PROXY_METHODS)
# Must stay the lower decorator so it registers before the catch-all, which would swallow chat paths.
@router.api_route('/{server_id}/chats/{chat_id}/{path:path}', methods=PROXY_METHODS)
async def proxy_terminal(
server_id: str,
path: str,
@ -151,7 +153,7 @@ async def proxy_terminal(
headers = {'X-User-Id': user.id}
# Forward per-session cwd tracking header
session_id = request.headers.get('x-session-id')
session_id = request.path_params.get('chat_id') or request.headers.get('x-session-id')
if session_id:
headers['X-Session-Id'] = session_id
if not terminal_context_available(connection, 'chat'):

View file

@ -355,6 +355,11 @@
let selectedTerminal: { url: string; key: string } | null = null;
let terminalChatContextPending = false;
let terminalChatContextHidden = false;
let scopedChatId: string | null = null;
$: portBaseUrl =
selectedTerminal && scopedChatId
? `${selectedTerminal.url}/chats/${encodeURIComponent(scopedChatId)}`
: (selectedTerminal?.url ?? '');
const chatContext = (terminal: any) => terminal?.contexts?.chat ?? {};
@ -364,6 +369,7 @@
: ($terminalServers?.[0] ?? null);
const chatConfig = chatContext(systemTerminal);
const chatScoped = !!systemTerminal && chatConfig?.context_id === 'chat_id';
scopedChatId = chatScoped ? chatId : null;
terminalChatContextHidden =
!!systemTerminal && (chatConfig === false || (chatScoped && isTemporaryChatId(chatId)));
terminalChatContextPending = chatScoped && !terminalChatContextHidden && !isSavedChatId(chatId);
@ -1824,7 +1830,7 @@
/>
{:else if previewPort !== null}
<PortPreview
baseUrl={selectedTerminal?.url ?? ''}
baseUrl={portBaseUrl}
port={previewPort}
overlay={overlay || isDraggingHandle}
onClose={() => {
@ -2099,7 +2105,7 @@
{#if selectedTerminal && !selectedFile && previewPort === null && !isSearching}
<div class="shrink-0 border-t border-gray-50 dark:border-gray-850/30">
<PortList
baseUrl={selectedTerminal.url}
baseUrl={portBaseUrl}
apiKey={selectedTerminal.key}
on:previewPort={(e) => {
selectedFile = null;