diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index ebfdd6993d..d2dd962641 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -277,6 +277,7 @@ def build_terminal_file_tool_result(
return None
mime_type, _ = mimetypes.guess_type(path)
mime_type = mime_type or 'application/octet-stream'
+ page = tool_result.get('page') or tool_function_params.get('page')
return {
**tool_result,
@@ -292,6 +293,7 @@ def build_terminal_file_tool_result(
'name': tool_result.get('name') or os.path.basename(path),
'mime_type': tool_result.get('mime_type') or tool_result.get('content_type') or mime_type,
'content_type': tool_result.get('content_type') or tool_result.get('mime_type') or mime_type,
+ **({'page': page} if page else {}),
}
@@ -1242,11 +1244,15 @@ async def terminal_event_handler(
pass
if isinstance(parsed, dict) and parsed.get('exists') is False:
return
+ page = tool_function_params.get('page')
await event_emitter(
{
'type': f'terminal:{tool_function_name}',
- 'data': {'path': path},
+ 'data': {
+ 'path': path,
+ **({'page': page} if page else {}),
+ },
}
)
elif tool_function_name in ('write_file', 'replace_file_content'):
diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py
index 19de805ea6..3c3d973480 100644
--- a/backend/open_webui/utils/tools.py
+++ b/backend/open_webui/utils/tools.py
@@ -968,6 +968,9 @@ def add_terminal_display_file_inline_param(spec: dict) -> dict:
properties['inline'] = {
'type': 'boolean',
'description': 'Show the file inline in the chat message instead of opening the file viewer.',
+ }
+ 'minimum': 1,
+ 'description': 'For PDF, DOCX, and PPTX files, open the preview at this 1-based page or slide number.',
}
return spec
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte
index 906637e75a..2c2cbb9b31 100644
--- a/src/lib/components/chat/Chat.svelte
+++ b/src/lib/components/chat/Chat.svelte
@@ -1139,7 +1139,7 @@
const terminalEventHandler = (type: string, data: any) => {
if (type === 'terminal:display_file') {
if (!data?.path) return;
- displayFileHandler(data.path, { showControls, showFileNavPath });
+ displayFileHandler(data.path, { showControls, showFileNavPath }, { page: data?.page });
} else if (type === 'terminal:write_file' || type === 'terminal:replace_file_content') {
if (!data?.path) return;
showFileNavDir.set(data.path);
diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte
index 56e423ff63..ec892a0c5d 100644
--- a/src/lib/components/chat/FileNav.svelte
+++ b/src/lib/components/chat/FileNav.svelte
@@ -37,6 +37,7 @@
} from '$lib/apis/terminal';
import { isCodeFile } from '$lib/utils/codeHighlight';
import { isSavedChatId, isTemporaryChatId } from '$lib/utils/chatId';
+ import { normalizeDocumentTargetPage } from '$lib/utils/documentPreview';
import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte';
@@ -302,6 +303,7 @@
let fileLoading = false;
let filePreviewRef: FilePreview;
let fileSearchTarget: FileSearchTarget | null = null;
+ let documentTargetPage: number | null = null;
// ── Office preview state ────────────────────────────────────────────
let fileOfficeHtml: string | null = null;
@@ -691,6 +693,7 @@
// ── File preview management ──────────────────────────────────────────
const clearFilePreview = () => {
fileSearchTarget = null;
+ documentTargetPage = null;
fileContent = null;
if (fileImageUrl) {
URL.revokeObjectURL(fileImageUrl);
@@ -810,7 +813,7 @@
}
};
- const openEntry = async (entry: FileEntry) => {
+ const openEntry = async (entry: FileEntry, options: { page?: unknown } = {}) => {
const fullPath =
'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry);
const parentPath = 'parentPath' in entry ? (entry as BrowserRow).parentPath : currentPath;
@@ -832,6 +835,7 @@
selectedFile = filePath;
fileLoading = true;
clearFilePreview();
+ documentTargetPage = normalizeDocumentTargetPage(options.page);
if (isImage(filePath)) {
const result = await downloadFileBlob(
@@ -1317,10 +1321,12 @@
let handledDisplayFile = false;
- const unsubFileNav = showFileNavPath.subscribe(async (filePath) => {
- if (!filePath || !selectedTerminal) return;
+ const unsubFileNav = showFileNavPath.subscribe(async (request) => {
+ if (!request || !selectedTerminal) return;
handledDisplayFile = true;
showFileNavPath.set(null);
+ let filePath = typeof request === 'string' ? request : request.path;
+ const targetPage = typeof request === 'string' ? null : request.page;
filePath = normalizePath(filePath);
if (!isInsideFileRoot(filePath)) {
await loadDir(fileRoot?.path ?? '/');
@@ -1337,10 +1343,10 @@
const entry = entries.find((e) => e.name === fileName);
if (entry) {
- await openEntry(entry);
+ await openEntry(entry, { page: targetPage });
} else {
// File may not be in listing; open it directly
- await openEntry({ name: fileName, type: 'file', size: 0 });
+ await openEntry({ name: fileName, type: 'file', size: 0 }, { page: targetPage });
}
});
@@ -1741,6 +1747,7 @@
{fileContent}
{fileOfficeHtml}
{fileOfficeSlides}
+ targetPage={documentTargetPage}
{excelSheetNames}
{selectedExcelSheet}
searchTarget={fileSearchTarget}
diff --git a/src/lib/components/chat/FileNav/FilePreview.svelte b/src/lib/components/chat/FileNav/FilePreview.svelte
index 8588c371cc..b5ea5507bb 100644
--- a/src/lib/components/chat/FileNav/FilePreview.svelte
+++ b/src/lib/components/chat/FileNav/FilePreview.svelte
@@ -39,6 +39,7 @@
export let fileOfficeHtml: string | null = null;
export let fileOfficeSlides: string[] | null = null;
export let currentSlide = 0;
+ export let targetPage: number | null = null;
export let excelSheetNames: string[] = [];
export let selectedExcelSheet = '';
export let onSheetChange: ((sheet: string) => void) | null = null;
@@ -320,11 +321,11 @@
{:else if filePdfData !== null}
-