This commit is contained in:
Timothy Jaeryang Baek committed 2026-08-08 15:47:10 -06:00
1 parent 8faaf2cd1e
commit 009999f363
12 files changed
+334 -32

No files matched your search

+24 -8
View File
@@ -34,6 +34,7 @@
import FileNav from './FileNav.svelte';
import PyodideFileNav from './PyodideFileNav.svelte';
import Overview from './Overview.svelte';
import { isSavedChatId } from '$lib/utils/chatId';
const i18n = getContext('i18n');
@@ -72,11 +73,26 @@
$: hasMessages = history?.messages && Object.keys(history.messages).length > 0;
$: showControlsTab = $user?.role === 'admin' || ($user?.permissions?.chat?.controls ?? true);
$: showFilesTab =
const chatContext = (terminal: any) => terminal?.contexts?.chat ?? {};
const chatContextAvailable = (terminal: any) => chatContext(terminal) !== false;
const chatContextNeedsSavedChat = (terminal: any) =>
chatContext(terminal)?.context_id === 'chat_id';
$: selectedSystemTerminal = ($terminalServers ?? []).find(
(t) => t.id && t.id === $selectedTerminalId
);
$: selectedSystemTerminalAvailable =
selectedSystemTerminal &&
chatContextAvailable(selectedSystemTerminal) &&
!(chatContextNeedsSavedChat(selectedSystemTerminal) && !isSavedChatId(chatId));
$: terminalFilesAvailable = !!(
($selectedTerminalId &&
(($terminalServers ?? []).some((t) => t.id && t.id === $selectedTerminalId) ||
$user?.role === 'admin' ||
($user?.permissions?.features?.direct_tool_servers ?? true))) ||
(selectedSystemTerminalAvailable ||
(!selectedSystemTerminal &&
($user?.role === 'admin' ||
($user?.permissions?.features?.direct_tool_servers ?? true)))))
);
$: showFilesTab =
terminalFilesAvailable ||
(codeInterpreterEnabled && $config?.code?.interpreter_engine !== 'jupyter');
$: showOverviewTab = hasMessages;
@@ -94,13 +110,13 @@
}
// Auto-switch to Files tab when display_file is triggered
$: if ($showFileNavPath) {
$: if ($showFileNavPath && terminalFilesAvailable) {
activeTab = 'files';
showControls.set(true);
}
// Auto-open Files tab when a terminal is selected (suppress panel open when full-screen)
$: if ($selectedTerminalId && showFilesTab) {
$: if ($selectedTerminalId && terminalFilesAvailable) {
activeTab = 'files';
if (largeScreen) {
showControls.set($settings?.showFilesOnTerminalSelect ?? true);
@@ -374,7 +390,7 @@
showMessage(node.data.message, true);
}}
/>
{:else if activeTab === 'files' && $selectedTerminalId}
{:else if activeTab === 'files' && terminalFilesAvailable && $selectedTerminalId}
<FileNav onAttach={handleTerminalAttach} {chatId} />
{:else if activeTab === 'files' && codeInterpreterEnabled}
<PyodideFileNav />
@@ -523,7 +539,7 @@
showMessage(node.data.message, true);
}}
/>
{:else if activeTab === 'files' && $selectedTerminalId}
{:else if activeTab === 'files' && terminalFilesAvailable && $selectedTerminalId}
<FileNav onAttach={handleTerminalAttach} overlay={dragged} {chatId} />
{:else if activeTab === 'files' && codeInterpreterEnabled}
<PyodideFileNav overlay={dragged} />
+29 -2
View File
@@ -34,6 +34,7 @@
import Document from '../icons/Document.svelte';
import PenAlt from '../icons/PenAlt.svelte';
import ZoomReset from '../icons/ZoomReset.svelte';
import { isSavedChatId, isTemporaryChatId } from '$lib/utils/chatId';
import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte';
@@ -231,11 +232,22 @@
// ── Terminal resolution ──────────────────────────────────────────────
let selectedTerminal: { url: string; key: string } | null = null;
let terminalChatContextPending = false;
let terminalChatContextHidden = false;
const chatContext = (terminal: any) => terminal?.contexts?.chat ?? {};
const getTerminal = (): { url: string; key: string } | null => {
const systemTerminal = $selectedTerminalId
? (($terminalServers ?? []).find((t) => t.id === $selectedTerminalId) ?? null)
: ($terminalServers?.[0] ?? null);
const chatConfig = chatContext(systemTerminal);
const chatScoped = !!systemTerminal && chatConfig?.context_id === 'chat_id';
terminalChatContextHidden =
!!systemTerminal && (chatConfig === false || (chatScoped && isTemporaryChatId(chatId)));
terminalChatContextPending =
chatScoped && !terminalChatContextHidden && !isSavedChatId(chatId);
if (terminalChatContextHidden || terminalChatContextPending) return null;
const userTerminal = ($settings?.terminalServers ?? []).find(
(s) => s.url === $selectedTerminalId
@@ -929,7 +941,13 @@
const onBlur = () => (shiftKey = false);
const onVisibilityChange = () => {
if (document.visibilityState === 'visible' && !selectedFile && selectedTerminal && !loading) {
if (
document.visibilityState === 'visible' &&
!selectedFile &&
selectedTerminal &&
!terminalChatContextPending &&
!loading
) {
loadDir(currentPath);
}
};
@@ -972,7 +990,16 @@
<svelte:window on:keydown={handleKeydown} on:click={handleWindowClick} />
{#if !selectedTerminal}
{#if terminalChatContextHidden}
<div class="hidden"></div>
{:else if terminalChatContextPending}
<div class="flex-1 flex flex-col items-center justify-center p-6 text-center">
<Folder className="size-6 text-gray-300 dark:text-gray-600 mb-2" />
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Start the chat to use this terminal.')}
</div>
</div>
{:else if !selectedTerminal}
<div class="flex-1 flex flex-col items-center justify-center p-6 text-center">
<Folder className="size-6 text-gray-300 dark:text-gray-600 mb-2" />
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
@@ -1,8 +1,16 @@
<script lang="ts">
import { getContext } from 'svelte';
import { settings, showSettings, terminalServers, selectedTerminalId, user } from '$lib/stores';
import {
chatId as activeChatId,
settings,
showSettings,
terminalServers,
selectedTerminalId,
user
} from '$lib/stores';
import { getToolServersData } from '$lib/apis';
import { isTemporaryChatId } from '$lib/utils/chatId';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
@@ -13,7 +21,14 @@
export let show = false;
$: systemTerminals = ($terminalServers ?? []).filter((t) => t.id);
const chatContext = (terminal: any) => terminal?.contexts?.chat ?? {};
$: systemTerminals = ($terminalServers ?? []).filter(
(t) =>
t.id &&
chatContext(t) !== false &&
!(isTemporaryChatId($activeChatId) && chatContext(t)?.context_id === 'chat_id')
);
$: directTerminals = ($settings?.terminalServers ?? []).filter((s) => s.url);
const refreshTerminalServersStore = async (servers: typeof directTerminals) => {
+24 -10
View File
@@ -1,15 +1,12 @@
<script lang="ts">
import { onMount, onDestroy, getContext } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
import { terminalServers, settings, selectedTerminalId, user } from '$lib/stores';
import { terminalServers, settings, selectedTerminalId } from '$lib/stores';
import { WEBUI_API_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let overlay = false;
export let chatId: string | null = null;
@@ -24,13 +21,19 @@
let pingInterval: ReturnType<typeof setInterval> | null = null;
// Resolve the active terminal server's info for the WebSocket URL
const getTerminalInfo = (): { serverId: string; baseUrl: string } | null => {
const getTerminalInfo = (): {
serverId: string;
baseUrl: string;
} | null => {
// System terminal (admin-configured, has an `id`)
const systemTerminals = ($terminalServers ?? []).filter((t: any) => t.id);
const systemMatch = systemTerminals.find((t: any) => t.id === $selectedTerminalId);
if (systemMatch) {
// For system terminals, WS goes through the Open WebUI backend proxy
return { serverId: systemMatch.id, baseUrl: WEBUI_API_BASE_URL };
return {
serverId: systemMatch.id,
baseUrl: WEBUI_API_BASE_URL
};
}
// Direct terminal (user-configured, matched by URL)
@@ -58,6 +61,7 @@
let sessionId: string;
let wsUrl: string;
let authToken: string;
let authChatId = '';
if (info.serverId === '__direct__') {
// Direct connection to open-terminal
@@ -98,6 +102,7 @@
const wsBase = base.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
wsUrl = `${wsBase}/terminals/${info.serverId}/api/terminals/${sessionId}`;
authChatId = chatId ?? '';
}
ws = new WebSocket(wsUrl);
@@ -106,7 +111,12 @@
ws.onopen = () => {
// First-message auth (no token in URL)
if (ws) {
ws.send(JSON.stringify({ type: 'auth', token: authToken.trim() }));
const authPayload: { type: string; token: string; chat_id?: string } = {
type: 'auth',
token: authToken.trim()
};
if (authChatId) authPayload.chat_id = authChatId;
ws.send(JSON.stringify(authPayload));
}
connected = true;
connecting = false;
@@ -259,7 +269,7 @@
};
// Reconnect when the selected terminal changes
$: if ($selectedTerminalId !== undefined && term) {
$: if (($selectedTerminalId, chatId, term)) {
// Clear the terminal screen and reconnect to the new server
disconnect();
term.clear();
@@ -282,5 +292,9 @@
</script>
<div class="h-full min-h-0 relative">
<div bind:this={terminalEl} class="absolute inset-0 px-0.5" class:pointer-events-none={overlay} />
<div
bind:this={terminalEl}
class="absolute inset-0 px-0.5"
class:pointer-events-none={overlay}
></div>
</div>