This commit is contained in:
Timothy Jaeryang Baek
2026-03-29 21:01:10 -05:00
parent 4777f4fa32
commit a06685a47b
8 changed files with 86 additions and 36 deletions
+4
View File
@@ -105,6 +105,10 @@ async def proxy_terminal(
target_url += f'?{request.query_params}'
headers = {'X-User-Id': user.id}
# Forward per-session cwd tracking header
session_id = request.headers.get('x-session-id')
if session_id:
headers['X-Session-Id'] = session_id
cookies = {}
auth_type = connection.get('auth_type', 'bearer')
+7
View File
@@ -1009,6 +1009,13 @@ async def get_terminal_tools(
# auth_type == "none": no Authorization header
system_prompt = server_data.get('system_prompt')
# Use chat_id as the per-session key for cwd tracking
metadata = extra_params.get('__metadata__', {})
session_id = metadata.get('chat_id')
if session_id:
headers['X-Session-Id'] = session_id
terminal_cwd = await get_terminal_cwd(connection.get('url', ''), headers, cookies)
tools_dict = {}
+3 -1
View File
@@ -454,7 +454,8 @@ export const executeToolServer = async (
url: string,
name: string,
params: Record<string, any>,
serverData: { openapi: any; info: any; specs: any }
serverData: { openapi: any; info: any; specs: any },
sessionId?: string
) => {
let error = null;
@@ -531,6 +532,7 @@ export const executeToolServer = async (
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
};
if (sessionId) headers['X-Session-Id'] = sessionId;
const requestOptions: RequestInit = {
method: httpMethod.toUpperCase(),
+12 -9
View File
@@ -45,11 +45,11 @@ export const getTerminalConfig = async (
return res.json().catch(() => null);
};
export const getCwd = async (baseUrl: string, apiKey: string): Promise<string | null> => {
export const getCwd = async (baseUrl: string, apiKey: string, sessionId?: string): Promise<string | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` }
}).catch(() => null);
const headers: Record<string, string> = { Authorization: `Bearer ${apiKey}` };
if (sessionId) headers['X-Session-Id'] = sessionId;
const res = await fetch(url, { headers }).catch(() => null);
if (!res || !res.ok) return null;
const json = await res.json().catch(() => null);
return json?.cwd ?? null;
@@ -218,15 +218,18 @@ export const deleteEntry = async (
export const setCwd = async (
baseUrl: string,
apiKey: string,
path: string
path: string,
sessionId?: string
): Promise<{ cwd: string } | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`;
const headers: Record<string, string> = {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
};
if (sessionId) headers['X-Session-Id'] = sessionId;
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
headers,
body: JSON.stringify({ path })
})
.then(async (res) => {
+2 -2
View File
@@ -362,7 +362,7 @@
onClose={() => showControls.set(false)}
/>
{:else if activeTab === 'files' && $selectedTerminalId}
<FileNav onAttach={handleTerminalAttach} />
<FileNav onAttach={handleTerminalAttach} {chatId} />
{:else if activeTab === 'files' && codeInterpreterEnabled}
<PyodideFileNav />
{:else}
@@ -513,7 +513,7 @@
onClose={() => showControls.set(false)}
/>
{:else if activeTab === 'files' && $selectedTerminalId}
<FileNav onAttach={handleTerminalAttach} overlay={dragged} />
<FileNav onAttach={handleTerminalAttach} overlay={dragged} {chatId} />
{:else if activeTab === 'files' && codeInterpreterEnabled}
<PyodideFileNav overlay={dragged} />
{:else}
+47 -19
View File
@@ -49,6 +49,7 @@
export let onAttach: ((blob: Blob, name: string, contentType: string) => void) | null = null;
export let overlay = false;
export let chatId: string | null = null;
// ── Terminal panel state ────────────────────────────────────────────
let terminalExpanded = false;
@@ -215,30 +216,48 @@
return url ? { url, key } : null;
};
// Detect terminal changes — the explicit store references ensure
// Detect terminal or chat changes — the explicit store references ensure
// Svelte re-runs this block when any of them update.
// The `mounted` flag prevents the initial run from racing with onMount.
let prevTerminalUrl = '';
let prevChatId = chatId;
let mounted = false;
$: {
($selectedTerminalId, $terminalServers, $settings);
const terminal = getTerminal();
selectedTerminal = terminal;
if (terminal && terminal.url !== prevTerminalUrl) {
prevTerminalUrl = terminal.url;
loading = true;
error = null;
entries = [];
(async () => {
// Discover server features (terminal enabled/disabled)
const config = await getTerminalConfig(terminal.url, terminal.key);
terminalEnabled = config?.features?.terminal !== false;
const chatChanged = chatId !== prevChatId;
const oldChatId = prevChatId;
if (chatChanged) prevChatId = chatId;
const rawCwd = await getCwd(terminal.url, terminal.key);
const cwd = rawCwd ? normalizePath(rawCwd) : null;
const dir = cwd ? (cwd.endsWith('/') ? cwd : cwd + '/') : '/';
savedPath = dir;
loadDir(dir);
})();
const terminalChanged = terminal && terminal.url !== prevTerminalUrl;
if (terminalChanged) prevTerminalUrl = terminal.url;
if (mounted && terminal) {
if (chatChanged && chatId && !oldChatId) {
// Chat just got created (null → real ID): persist the current
// browsed path as the new session's cwd — don't re-fetch.
setCwd(terminal.url, terminal.key, savedPath, chatId);
} else if (terminalChanged || chatChanged) {
// Terminal switched, new chat started, or switched between
// existing chats — re-fetch the session cwd.
loading = true;
error = null;
entries = [];
(async () => {
if (terminalChanged) {
const config = await getTerminalConfig(terminal.url, terminal.key);
terminalEnabled = config?.features?.terminal !== false;
}
const rawCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined);
const cwd = rawCwd ? normalizePath(rawCwd) : null;
const dir = cwd ? (cwd.endsWith('/') ? cwd : cwd + '/') : '/';
savedPath = dir;
loadDir(dir);
})();
}
}
}
@@ -316,7 +335,7 @@
loading = false;
// Set working directory on the terminal server (fire-and-forget)
setCwd(terminal.url, terminal.key, path);
setCwd(terminal.url, terminal.key, path, chatId ?? undefined);
if (result === null) {
error =
@@ -736,14 +755,22 @@
if (!handledDisplayFile) {
loading = true;
if (savedPath === '/') {
const rawCwd = await getCwd(terminal.url, terminal.key);
// Discover server features on initial mount
const config = await getTerminalConfig(terminal.url, terminal.key);
terminalEnabled = config?.features?.terminal !== false;
if (chatId || savedPath === '/') {
// Fetch session-specific cwd from the server (or global default for new chats)
const rawCwd = await getCwd(terminal.url, terminal.key, chatId ?? undefined);
const cwd = rawCwd ? normalizePath(rawCwd) : null;
if (cwd) savedPath = cwd.endsWith('/') ? cwd : cwd + '/';
}
loadDir(savedPath);
}
mounted = true;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Shift') shiftKey = true;
};
@@ -1368,6 +1395,7 @@
overlay={overlay || isDraggingHandle}
bind:connected={terminalConnected}
bind:connecting={terminalConnecting}
{chatId}
/>
</div>
{/if}
+7 -2
View File
@@ -12,6 +12,7 @@
const i18n = getContext('i18n');
export let overlay = false;
export let chatId: string | null = null;
let terminalEl: HTMLDivElement;
let term: Terminal | null = null;
@@ -67,9 +68,11 @@
authToken = apiKey;
// Create session
const createHeaders: Record<string, string> = { Authorization: `Bearer ${apiKey}` };
if (chatId) createHeaders['X-Session-Id'] = chatId;
const res = await fetch(`${base}/api/terminals`, {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` }
headers: createHeaders
});
if (!res.ok) throw new Error(`Failed to create session: ${res.status}`);
const session = await res.json();
@@ -83,9 +86,11 @@
authToken = token;
// Create session via proxy
const proxyHeaders: Record<string, string> = { Authorization: `Bearer ${token}` };
if (chatId) proxyHeaders['X-Session-Id'] = chatId;
const res = await fetch(`${base}/terminals/${info.serverId}/api/terminals`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` }
headers: proxyHeaders
});
if (!res.ok) throw new Error(`Failed to create session: ${res.status}`);
const session = await res.json();
+4 -3
View File
@@ -375,7 +375,7 @@
return { toolServer, toolServerData, token };
};
const executeTool = async (data, cb) => {
const executeTool = async (data, cb, chatId) => {
const { toolServer, toolServerData, token } = resolveToolServer(data.server?.url);
console.log('executeTool', data, toolServer);
@@ -386,7 +386,8 @@
toolServer.url,
data?.name,
data?.params,
toolServerData
toolServerData,
chatId
);
console.log('executeToolServer', res);
@@ -485,7 +486,7 @@
executePythonAsWorker(data.id, data.code, cb, data.files || []);
} else if (type === 'execute:tool') {
console.log('execute:tool', data);
executeTool(data, cb);
executeTool(data, cb, event.chat_id);
} else if (type === 'request:chat:completion') {
console.log(data, $socket.id);
const { session_id, channel, form_data, model } = data;