This commit is contained in:
Timothy Jaeryang Baek
2026-08-19 22:48:32 -07:00
parent 46dce79eb0
commit 8a42aa53e8
11 changed files with 147 additions and 12 deletions
+2
View File
@@ -27,6 +27,7 @@ from open_webui.utils.terminals import (
terminal_context_available,
terminal_context_config,
terminal_context_id,
terminal_chat_uploads,
terminal_contexts,
)
from open_webui.utils.tools import bearer_auth_header, normalize_bearer_token
@@ -87,6 +88,7 @@ async def list_terminal_servers(request: Request, user=Depends(get_verified_user
'url': connection.get('url', ''),
'name': connection.get('name', ''),
'contexts': terminal_contexts(connection),
'config': {'chat_uploads': terminal_chat_uploads(connection)},
}
for connection in connections
if connection.get('enabled', True) and await has_connection_access(user, connection, user_group_ids)
+7
View File
@@ -8,6 +8,7 @@ TERMINAL_CONTEXT_HEADER = 'X-Terminal-Context-Id'
TERMINAL_CONTEXT_DEFAULT = 'default'
TERMINAL_CONTEXT_TYPES = {'chat', 'automation'}
TERMINAL_CONTEXT_ID_SOURCES = {'chat': 'chat_id', 'automation': 'automation_id'}
TERMINAL_CHAT_UPLOAD_MODES = {'default', 'filesystem'}
def is_terminal_orchestrator(connection: dict) -> bool:
@@ -104,3 +105,9 @@ def terminal_contexts(connection: dict) -> dict:
else:
result[context] = {}
return result
def terminal_chat_uploads(connection: dict) -> str:
"""Return normalized main-chat upload behavior for this connection."""
value = (connection.get('config') or {}).get('chat_uploads')
return value if value in TERMINAL_CHAT_UPLOAD_MODES else 'default'
+4
View File
@@ -43,6 +43,10 @@ export type TerminalServer = {
url: string;
name: string;
contexts?: Record<string, false | { context_id?: string }>;
config?: {
chat_uploads?: 'default' | 'filesystem';
[key: string]: unknown;
};
};
export const getTerminalServers = async (token: string): Promise<TerminalServer[]> => {
@@ -36,6 +36,7 @@
let auth_type = 'bearer';
let path = '/openapi.json';
let enabled = false;
let chatUploads: 'default' | 'filesystem' = 'default';
let chatContextMode: 'default' | 'chat_id' | 'off' = 'default';
let automationContextMode: 'default' | 'automation_id' | 'off' = 'default';
let showAdvanced = false;
@@ -80,6 +81,7 @@
auth_type = connection?.auth_type ?? 'bearer';
path = connection?.path ?? '/openapi.json';
enabled = connection?.enabled ?? true;
chatUploads = connection?.config?.chat_uploads === 'filesystem' ? 'filesystem' : 'default';
accessGrants = connection?.config?.access_grants ?? [];
// Restore policy state
@@ -125,6 +127,7 @@
auth_type = 'bearer';
path = '/openapi.json';
enabled = false;
chatUploads = 'default';
accessGrants = [];
chatContextMode = 'default';
automationContextMode = 'default';
@@ -373,6 +376,14 @@
contexts.automation = { context_id: 'automation_id' };
}
const useContexts = !direct && serverType === 'orchestrator' && Object.keys(contexts).length > 0;
const connectionConfig: Record<string, any> =
connection?.config && typeof connection.config === 'object' ? { ...connection.config } : {};
if (!direct) connectionConfig.access_grants = accessGrants;
else delete connectionConfig.access_grants;
if (useContexts) connectionConfig.contexts = contexts;
else delete connectionConfig.contexts;
if (chatUploads === 'filesystem') connectionConfig.chat_uploads = 'filesystem';
else delete connectionConfig.chat_uploads;
const result = {
...(!direct && id.trim() ? { id: id.trim() } : {}),
@@ -382,10 +393,7 @@
path,
auth_type,
enabled: enabled,
config: {
...(!direct ? { access_grants: accessGrants } : {}),
...(useContexts ? { contexts } : {})
},
config: connectionConfig,
// Policy fields
...(serverType ? { server_type: serverType } : {}),
...(serverType === 'orchestrator' && policyId ? { policy_id: policyId } : {})
@@ -532,6 +540,26 @@
</Tooltip>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class="flex justify-between mb-0.5">
<label for="terminal-chat-uploads" class={`text-xs text-gray-500`}
>{$i18n.t('Chat Uploads')}</label
>
</div>
<div class="flex flex-1 items-center">
<select
id="terminal-chat-uploads"
class={`w-full text-sm ${selectClass}`}
bind:value={chatUploads}
>
<option value="default">{$i18n.t('Default')}</option>
<option value="filesystem">{$i18n.t('Filesystem')}</option>
</select>
</div>
</div>
</div>
<!-- Policy section (orchestrator only, admin only) -->
{#if serverType === 'orchestrator' && !direct}
<button
@@ -94,7 +94,8 @@
url: `${WEBUI_API_BASE_URL}/terminals/${t.id}`,
name: t.name,
key: localStorage.token,
contexts: t.contexts ?? {}
contexts: t.contexts ?? {},
config: t.config ?? {}
}));
terminalServers.set([...existingDirectTerminals, ...systemEntries] as any);
}
+76 -3
View File
@@ -56,6 +56,7 @@
getWeekday
} from '$lib/utils';
import { uploadFile } from '$lib/apis/files';
import { getCwd, uploadToTerminal } from '$lib/apis/terminal';
import { generateAutoCompletion } from '$lib/apis';
import { deleteFileById } from '$lib/apis/files';
import { getChatById } from '$lib/apis/chats';
@@ -860,19 +861,43 @@
}
};
const getFilesystemUploadTerminal = (
selectedId = $selectedTerminalId,
servers: any[] | null = $terminalServers,
settingsValue: any = $settings
) => {
if (!selectedId) return null;
const systemTerminal = (servers ?? []).find(
(t: any) => t.id && t.id === selectedId && t.config?.chat_uploads === 'filesystem'
);
if (systemTerminal) return systemTerminal;
return (
(settingsValue?.terminalServers ?? []).find(
(t: any) =>
t.url === selectedId &&
t.enabled &&
t.config?.chat_uploads === 'filesystem'
) ?? null
);
};
const uploadFileHandler = async (file, process = true, itemData = {}) => {
if ($_user?.role !== 'admin' && !($_user?.permissions?.chat?.file_upload ?? true)) {
toast.error($i18n.t('You do not have permission to upload files.'));
return null;
}
if (fileUploadCapableModels.length !== selectedModelIds.length) {
const filesystemUploadTerminal = getFilesystemUploadTerminal();
if (!filesystemUploadTerminal && fileUploadCapableModels.length !== selectedModelIds.length) {
toast.error($i18n.t('Model(s) do not support file upload'));
return null;
}
const tempItemId = uuidv4();
const fileItem = {
const fileItem: any = {
type: 'file',
file: '',
id: null,
@@ -896,6 +921,48 @@
files = [...files, fileItem];
if (filesystemUploadTerminal) {
try {
const cwd =
(await getCwd(
filesystemUploadTerminal.url,
filesystemUploadTerminal.key,
chatId || undefined
))?.cwd || '/';
const uploadedFile = await uploadToTerminal(
filesystemUploadTerminal.url,
filesystemUploadTerminal.key,
cwd,
file,
chatId || undefined
);
if (uploadedFile) {
fileItem.type = 'terminal_file';
fileItem.status = 'uploaded';
fileItem.id = uploadedFile.path;
fileItem.path = uploadedFile.path;
fileItem.url = uploadedFile.path;
fileItem.size = uploadedFile.size ?? file.size;
fileItem.file = uploadedFile;
files = files;
} else {
fileItem.status = 'error';
fileItem.error = $i18n.t('Failed to upload file.');
toast.error(fileItem.error);
files = files.filter((item) => item?.itemId !== tempItemId);
}
} catch (e) {
fileItem.status = 'error';
fileItem.error = `${e}`;
toast.error(`${e}`);
files = files.filter((item) => item?.itemId !== tempItemId);
} finally {
onUpdate({ file: fileItem });
}
return;
}
if (!$temporaryChatEnabled) {
try {
// If the file is an audio file, provide the language for STT.
@@ -2078,7 +2145,13 @@
<InputMenu
bind:files
selectedModels={selectedModelIds}
{fileUploadCapableModels}
fileUploadCapableModels={getFilesystemUploadTerminal(
$selectedTerminalId,
$terminalServers,
$settings
)
? selectedModelIds
: fileUploadCapableModels}
{toolApprovalMode}
{onToolApprovalModeChange}
{screenCaptureHandler}
@@ -47,7 +47,14 @@
}))
);
data = data.filter((d) => d && !d.error);
terminalServers.set([...data, ...existingSystemTerminals]);
terminalServers.set([
...data.map((d, i) => ({
...d,
key: activeTerminals[i]?.key ?? '',
config: activeTerminals[i]?.config ?? d?.config ?? {}
})),
...existingSystemTerminals
]);
} else {
terminalServers.set(existingSystemTerminals);
}
@@ -74,7 +74,14 @@
}))
);
terminalServersData = terminalServersData.filter((data: any) => data && !data.error);
terminalServers.set([...terminalServersData, ...existingSystemTerminals] as any);
terminalServers.set([
...terminalServersData.map((data: any, i: number) => ({
...data,
key: activeTerminals[i]?.key ?? '',
config: activeTerminals[i]?.config ?? data?.config ?? {}
})),
...existingSystemTerminals
] as any);
} else {
terminalServers.set(existingSystemTerminals as any);
}
@@ -434,6 +434,7 @@
"Chat Permissions": "",
"Chat Tags Auto-Generation": "",
"Chat unshared successfully.": "",
"Chat Uploads": "",
"Chat Variables": "",
"Chat Variables have conflicting model definitions": "",
"chats": "",
@@ -1236,6 +1237,7 @@
"File uploaded successfully": "",
"Filename": "",
"Files": "",
"Filesystem": "",
"Fill the required fields first.": "",
"Fill the source fields and test query first.": "",
"Filter": "",
@@ -435,6 +435,7 @@
"Chat Permissions": "",
"Chat Tags Auto-Generation": "",
"Chat unshared successfully.": "",
"Chat Uploads": "",
"Chat Variables": "",
"Chat Variables have conflicting model definitions": "",
"chats": "",
@@ -1238,6 +1239,7 @@
"File uploaded successfully": "",
"Filename": "",
"Files": "",
"Filesystem": "",
"Fill the required fields first.": "",
"Fill the source fields and test query first.": "",
"Filter": "",
+4 -2
View File
@@ -161,7 +161,8 @@
})
.map((data, i) => ({
...data,
key: enabledTerminals[i]?.key ?? ''
key: enabledTerminals[i]?.key ?? '',
config: enabledTerminals[i]?.config ?? data?.config ?? {}
}))
: []),
// Store with proxy URL and session key for FileNav file browsing
@@ -170,7 +171,8 @@
url: `${WEBUI_API_BASE_URL}/terminals/${t.id}`,
name: t.name,
key: localStorage.token,
contexts: t.contexts ?? {}
contexts: t.contexts ?? {},
config: t.config ?? {}
}))
]);
};