From af34515131d9efd41cfeae5c1ea50a7f7ed84443 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 16 Oct 2026 04:07:18 +0400 Subject: [PATCH] refac --- src/lib/apis/terminal/index.ts | 30 ++ src/lib/components/chat/Chat.svelte | 108 +++- src/lib/components/chat/ChatControls.svelte | 7 +- src/lib/components/chat/FileNav.svelte | 483 ++++++++++++++++-- .../chat/FileNav/CommentFilePreview.svelte | 19 +- .../chat/FileNav/FileCodeEditor.svelte | 12 +- .../chat/FileNav/FilePreview.svelte | 76 ++- .../chat/FileNav/MarkdownDocument.svelte | 119 ++++- src/lib/components/chat/MessageInput.svelte | 10 +- .../MessageInput/Commands/AtCommands.svelte | 16 +- .../Markdown/MarkdownInlineTokens.svelte | 17 +- src/lib/stores/index.ts | 2 +- src/lib/types/index.ts | 1 + src/routes/+layout.svelte | 2 +- 14 files changed, 804 insertions(+), 98 deletions(-) diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index f5b8a16275..4dca975669 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -1,6 +1,36 @@ import { get } from 'svelte/store'; import { config } from '$lib/stores'; import { checkSourceSize, readSourceResponse } from './sourceText'; +// The same attachment is used by @mentions, terminal uploads, and Files-pane requests. +export type FilesystemFile = { + type: 'filesystem'; + id: string; + path: string; + url: string; + name: string; + status: string; + size?: number; + modified?: number; + terminal_id?: string; + session_id?: string | null; +}; +export const filesystemFile = ( + entry: { path: string; name: string; size?: number; modified?: number }, + terminalId: string, + sessionId: string | null +): FilesystemFile => ({ + type: 'filesystem', + id: entry.path, + path: entry.path, + url: entry.path, + name: entry.name, + size: entry.size, + modified: entry.modified, + status: 'processed', + terminal_id: terminalId, + session_id: sessionId +}); + export type FileEntry = { name: string; type: 'file' | 'directory'; diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index a7cafd0c4c..ee2a58bfab 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -95,6 +95,7 @@ updateChatById, updateChatFolderIdById } from '$lib/apis/chats'; + import type { FilesystemFile } from '$lib/apis/terminal'; import { generateOpenAIChatCompletion, getErrorMessage } from '$lib/apis/openai'; import { processUrl, processWebSearch } from '$lib/apis/retrieval'; import { @@ -475,6 +476,20 @@ let chatFiles = []; let files: any[] = []; let terminalContext: TerminalContext | null = null; + const askFile = async (instruction: string, file: FilesystemFile) => { + const attachments = [structuredClone(file)]; + const last = history.currentId ? history.messages[history.currentId] : null; + if (last?.role === 'assistant' && !last.done) { + chatRequestQueues.update((queues) => ({ + ...queues, + [$chatId]: [ + ...(queues[$chatId] ?? []), + { id: uuidv4(), prompt: instruction, files: attachments } + ] + })); + } else await submitPrompt(instruction, attachments); + }; + let params = {}; let chatVariables = {}; let showChatVariablesModal = false; @@ -1267,7 +1282,16 @@ }); }; - const terminalEventHandler = (type: string, data: any) => { + const terminalEventHandler = (type: string, data: any, message: any) => { + const attachment = history.messages[message?.parentId]?.files?.find( + (file: any) => file.type === 'filesystem' + ); + if (attachment) { + if (attachment.terminal_id && attachment.terminal_id !== $selectedTerminalId) return; + if (type === 'terminal:display_file' && terminalContext?.file?.path === data.path) + showFileNavDir.set(data.path); + return; + } if (type === 'terminal:display_file') { if (!data?.path) return; if ($settings?.terminalFileDisplay === 'inline') return; @@ -1275,7 +1299,7 @@ } else if (type === 'terminal:write_file' || type === 'terminal:replace_file_content') { if (!data?.path) return; showFileNavDir.set(data.path); - } else if (type === 'terminal:run_command') { + } else if (type === 'terminal:run_command' && !terminalContext?.file) { showFileNavDir.set('/'); } }; @@ -1760,7 +1784,7 @@ eventConfirmationInputType = data?.input?.type ?? data?.type ?? ''; eventConfirmationInputOptions = data?.input?.options ?? data?.options ?? []; } else if (type.startsWith('terminal:')) { - terminalEventHandler(type, data); + terminalEventHandler(type, data, message); } else { console.log('Unknown message type', data); } @@ -3068,10 +3092,10 @@ let processingQueueChats = new Set(); const processNextInQueue = async (targetChatId: string) => { - if (processingQueueChats.has(targetChatId)) return; + if (processingQueueChats.has(targetChatId) || targetChatId !== $chatId) return; const queue = $chatRequestQueues[targetChatId]; - if (!queue || queue.length === 0) return; + if (!queue || queue.length === 0 || queue[0].error) return; const lastMessage = history.currentId ? history.messages[history.currentId] : null; if ( (lastMessage && lastMessage.role === 'assistant' && !lastMessage.done) || @@ -3083,7 +3107,11 @@ } processingQueueChats.add(targetChatId); - const queuedMessages = [...queue]; + const boundary = queue.findIndex((entry) => + entry.files.some((file: any) => file.type === 'filesystem') + ); + const queuedMessages = + boundary === 0 ? queue.slice(0, 1) : boundary < 0 ? [...queue] : queue.slice(0, boundary); const queuedMessageIds = new Set(queuedMessages.map((m) => m.id)); try { const combinedPrompt = queuedMessages.map((m) => m.prompt).join('\n\n'); @@ -3099,7 +3127,10 @@ console.error(error); chatRequestQueues.update((q) => ({ ...q, - [targetChatId]: [...queuedMessages, ...(q[targetChatId] ?? [])] + [targetChatId]: [ + ...queuedMessages.map((entry) => ({ ...entry, error: String(error) })), + ...(q[targetChatId] ?? []) + ] })); } finally { processingQueueChats.delete(targetChatId); @@ -3125,6 +3156,12 @@ await stopResponse(false); await tick(); await submitPrompt(item.prompt, item.files); + } catch (error) { + toast.error(String(error)); + chatRequestQueues.update((q) => ({ + ...q, + [targetChatId]: [{ ...item, error: String(error) }, ...(q[targetChatId] ?? [])] + })); } finally { processingQueueChats.delete(targetChatId); // Completion can arrive before submitPrompt returns, while the queue is locked. @@ -3575,7 +3612,9 @@ saveSessionSelectedModels(); - return await sendMessage(history, userMessageId, { modelId: bridgeRequest?.modelId ?? null }); + return await sendMessage(history, userMessageId, { + modelId: bridgeRequest?.modelId ?? null + }); }; const handleManualCompact = async () => { @@ -3859,7 +3898,11 @@ files = []; messageInput?.setText(''); - return (await submitPrompt(userPrompt, _files, bridgeRequest)) ?? { status: 'rejected' }; + return ( + (await submitPrompt(userPrompt, _files, bridgeRequest)) ?? { + status: 'rejected' + } + ); }; const sendMessage = async ( @@ -3877,6 +3920,21 @@ regenerationPrompt?: string | null; } = {} ) => { + const parent = _history.messages[parentId]; + if (parent?.files?.some((file: any) => file.type === 'filesystem')) { + const attached = parent.files.filter((file: any) => file.type === 'filesystem'); + const terminal = attached.find((file: any) => file.terminal_id)?.terminal_id; + if ( + attached.some( + (file: any) => + (file.terminal_id && file.terminal_id !== terminal) || + (file.session_id && file.session_id !== $chatId) + ) + ) + throw new Error('Attached files must belong to this chat and the same terminal.'); + if (terminal && !isTerminalAvailable(terminal)) + throw new Error('The attached file’s terminal is unavailable.'); + } if (autoScroll) { scrollToBottom(); } @@ -4095,6 +4153,10 @@ ) => { const responseMessage = _history.messages[responseMessageId]; const userMessage = _history.messages[responseMessage.parentId]; + const filesystemAttachment = userMessage?.files?.find( + (file: any) => file.type === 'filesystem' + ); + const requestTerminalId = filesystemAttachment?.terminal_id ?? $selectedTerminalId; const chatMessageFiles = _messages .filter((message) => message.files) @@ -4257,7 +4319,7 @@ : $config?.features?.enable_tools ), skill_ids: skillIds.length > 0 ? skillIds : undefined, - terminal_id: terminalEnabled && $selectedTerminalId ? $selectedTerminalId : undefined, + terminal_id: terminalEnabled && requestTerminalId ? requestTerminalId : undefined, tool_servers: $config?.features?.enable_tool_servers ? [ ...($toolServers ?? []).filter( @@ -4266,7 +4328,10 @@ // Direct terminal servers — always included when enabled (not routed through selectedToolIds) ...(terminalEnabled ? ($terminalServers ?? []) - .filter((server) => !server.id) + .filter( + (server) => + !server.id && (!filesystemAttachment || server.url === requestTerminalId) + ) .map((server) => ({ ...server, is_terminal: true })) : []) ] @@ -4335,6 +4400,8 @@ return null; }); + if (filesystemAttachment && (!res || res.error)) + throw new Error('File request failed. Retry from the message or queue.'); if (res) { if (res.error) { await handleOpenAIError(res.error, responseMessage); @@ -4353,9 +4420,25 @@ const { [_chatId]: pendingQueue, ...rest } = q; return { ...rest, - [res.chat_id]: [...pendingQueue, ...(rest[res.chat_id] ?? [])] + [res.chat_id]: [ + ...pendingQueue.map((entry) => ({ + ...entry, + files: entry.files.map((file) => + file.type === 'filesystem' && file.session_id == null + ? { ...file, session_id: res.chat_id } + : file + ) + })), + ...(rest[res.chat_id] ?? []) + ] }; }); + for (const file of userMessage?.files ?? []) + if (file.type === 'filesystem' && file.session_id == null) + file.session_id = res.chat_id; + for (const file of history.messages[userMessage.id]?.files ?? []) + if (file.type === 'filesystem' && file.session_id == null) + file.session_id = res.chat_id; await chatId.set(res.chat_id); if (!$temporaryChatEnabled && !embedded) { // Carry the picks made on the new-chat page over to this chat @@ -5401,6 +5484,7 @@ return a; }, [])} submitPrompt={submitHandler} + onAskFile={askFile} {stopResponse} {bridge} bind:callMode diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte index 49712c45ec..2b07baa9b5 100644 --- a/src/lib/components/chat/ChatControls.svelte +++ b/src/lib/components/chat/ChatControls.svelte @@ -45,6 +45,9 @@ export let callMode = 'current'; export let eventTarget: EventTarget; export let submitPrompt: Function; + export let onAskFile: + | ((instruction: string, file: import('$lib/apis/terminal').FilesystemFile) => Promise) + | null = null; export let stopResponse: Function; export let showMessage: Function; export let files; @@ -300,7 +303,7 @@ }} /> {:else if activeTab === 'files' && terminalFilesAvailable && $selectedTerminalId} - + {:else if activeTab === 'files' && codeInterpreterEnabled} {:else} @@ -430,7 +433,7 @@ }} /> {:else if activeTab === 'files' && terminalFilesAvailable && $selectedTerminalId} - + {:else if activeTab === 'files' && codeInterpreterEnabled} {:else} diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 241fa8be4c..2af270a705 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -13,6 +13,9 @@ -
- -
+{#key resourceVersion} + +
+ +
+{/key}