fix: various UI and server improvements
This commit is contained in:
@@ -969,12 +969,14 @@ const useFileReferenceInteractions = ({
|
||||
readFile,
|
||||
editor,
|
||||
preferRuntimeEditor,
|
||||
skipValidation = false,
|
||||
}: {
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
effectiveDirectory: string;
|
||||
readFile?: (path: string) => Promise<{ content: string; path: string }>;
|
||||
editor?: EditorAPI;
|
||||
preferRuntimeEditor?: boolean;
|
||||
skipValidation?: boolean;
|
||||
}) => {
|
||||
const validationCacheRef = React.useRef<Map<string, boolean>>(new Map());
|
||||
const inFlightValidationsRef = React.useRef<Map<string, Promise<boolean>>>(new Map());
|
||||
@@ -1001,14 +1003,26 @@ const useFileReferenceInteractions = ({
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
const silentReadFile = async (path: string): Promise<boolean> => {
|
||||
if (!readFile) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await readFile(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkPromise = (async () => {
|
||||
try {
|
||||
if (!readFile) {
|
||||
return false;
|
||||
}
|
||||
await readFile(resolvedPath);
|
||||
cache.set(resolvedPath, true);
|
||||
return true;
|
||||
const exists = await silentReadFile(resolvedPath);
|
||||
cache.set(resolvedPath, exists);
|
||||
return exists;
|
||||
} catch {
|
||||
cache.set(resolvedPath, false);
|
||||
return false;
|
||||
@@ -1193,9 +1207,14 @@ const useFileReferenceInteractions = ({
|
||||
void openFileReference(target);
|
||||
};
|
||||
|
||||
void annotateFileLinks();
|
||||
if (!skipValidation) {
|
||||
void annotateFileLinks();
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (skipValidation) {
|
||||
return;
|
||||
}
|
||||
if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
|
||||
window.clearTimeout(annotationDebounceRef.current);
|
||||
}
|
||||
@@ -1224,7 +1243,7 @@ const useFileReferenceInteractions = ({
|
||||
container.removeEventListener('click', handleClick);
|
||||
container.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [containerRef, editor, effectiveDirectory, preferRuntimeEditor, readFile]);
|
||||
}, [containerRef, editor, effectiveDirectory, preferRuntimeEditor, readFile, skipValidation]);
|
||||
};
|
||||
|
||||
const useMermaidInlineInteractions = ({
|
||||
@@ -1341,6 +1360,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
readFile: files.readFile,
|
||||
editor,
|
||||
preferRuntimeEditor: runtime.isVSCode,
|
||||
skipValidation: !isStreaming,
|
||||
});
|
||||
|
||||
const shikiThemes = useMarkdownShikiThemes();
|
||||
@@ -1417,6 +1437,7 @@ export const SimpleMarkdownRenderer: React.FC<{
|
||||
readFile: files.readFile,
|
||||
editor,
|
||||
preferRuntimeEditor: runtime.isVSCode,
|
||||
skipValidation: true,
|
||||
});
|
||||
|
||||
const shikiThemes = useMarkdownShikiThemes();
|
||||
|
||||
@@ -536,7 +536,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
const handleOpenFile = React.useCallback(async (node: FileNode) => {
|
||||
if (!root) return;
|
||||
|
||||
const openValidation = await validateContextFileOpen(files, node.path);
|
||||
const openValidation = await validateContextFileOpen(files, node.path, root);
|
||||
if (!openValidation.ok) {
|
||||
toast.error(getContextFileOpenFailureMessage(openValidation.reason));
|
||||
return;
|
||||
|
||||
@@ -1410,7 +1410,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
: getFirstChangedModifiedLine(diffForNavigation.original, diffForNavigation.modified));
|
||||
|
||||
const absolutePath = toAbsolutePath(effectiveDirectory, filePath);
|
||||
const openValidation = await validateContextFileOpen(files, absolutePath);
|
||||
const openValidation = await validateContextFileOpen(files, absolutePath, effectiveDirectory ?? undefined);
|
||||
if (!openValidation.ok) {
|
||||
toast.error(getContextFileOpenFailureMessage(openValidation.reason));
|
||||
return;
|
||||
|
||||
@@ -1080,13 +1080,16 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return result.content ?? '';
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
|
||||
const directory = currentDirectory || undefined;
|
||||
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`, {
|
||||
headers: directory ? { 'x-opencode-directory': directory } : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error((error as { error?: string }).error || 'Failed to read file');
|
||||
}
|
||||
return response.text();
|
||||
}, [files]);
|
||||
}, [files, currentDirectory]);
|
||||
|
||||
const displayedContent = React.useMemo(() => {
|
||||
return fileContent.length > MAX_VIEW_CHARS
|
||||
@@ -1929,7 +1932,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
: desktopImageSrc)
|
||||
: (isSelectedSvg
|
||||
? `data:${getImageMimeType(selectedFile.path)};utf8,${encodeURIComponent(fileContent)}`
|
||||
: `/api/fs/raw?path=${encodeURIComponent(selectedFile.path)}`))
|
||||
: `/api/fs/raw?path=${encodeURIComponent(selectedFile.path)}&directory=${encodeURIComponent(currentDirectory || '')}`))
|
||||
: '';
|
||||
|
||||
|
||||
|
||||
@@ -263,7 +263,9 @@ export const PlanView: React.FC = () => {
|
||||
return result?.content ?? '';
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
|
||||
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`, {
|
||||
headers: sessionDirectory ? { 'x-opencode-directory': sessionDirectory } : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to read plan file (${response.status})`);
|
||||
}
|
||||
|
||||
@@ -24,13 +24,14 @@ const classifyReadError = (error: unknown): ContextFileOpenFailureReason => {
|
||||
return 'unreadable';
|
||||
};
|
||||
|
||||
const readFileContent = async (files: FilesAPI, path: string): Promise<string> => {
|
||||
const readFileContent = async (files: FilesAPI, path: string, directory?: string): Promise<string> => {
|
||||
if (files.readFile) {
|
||||
const result = await files.readFile(path);
|
||||
return result.content ?? '';
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`);
|
||||
const headers = directory ? { 'x-opencode-directory': directory } : undefined;
|
||||
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}`, { headers });
|
||||
if (!response.ok) {
|
||||
const errorPayload = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error((errorPayload as { error?: string }).error || 'Failed to read file');
|
||||
@@ -39,9 +40,9 @@ const readFileContent = async (files: FilesAPI, path: string): Promise<string> =
|
||||
return response.text();
|
||||
};
|
||||
|
||||
export const validateContextFileOpen = async (files: FilesAPI, path: string): Promise<ContextFileOpenValidationResult> => {
|
||||
export const validateContextFileOpen = async (files: FilesAPI, path: string, directory?: string): Promise<ContextFileOpenValidationResult> => {
|
||||
try {
|
||||
const content = await readFileContent(files, path);
|
||||
const content = await readFileContent(files, path, directory);
|
||||
const lineCount = countLinesWithLimit(content, MAX_OPEN_FILE_LINES);
|
||||
if (lineCount > MAX_OPEN_FILE_LINES) {
|
||||
return { ok: false, reason: 'too-large' };
|
||||
|
||||
@@ -1421,7 +1421,7 @@ class OpencodeService {
|
||||
return;
|
||||
}
|
||||
const elapsed = Date.now() - this.globalSseLastFlushAt;
|
||||
const delay = Math.max(0, 16 - elapsed);
|
||||
const delay = 8;
|
||||
this.globalSseFlushTimer = setTimeout(this.flushGlobalSseQueue, delay);
|
||||
}
|
||||
|
||||
@@ -1516,7 +1516,12 @@ class OpencodeService {
|
||||
if ((error as Error)?.name === 'AbortError' || abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
console.error('[OpencodeClient] Global SSE stream error (will retry):', error);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
const isNetworkError = errorMsg.includes('network error') || (error as any)?.name === 'TypeError';
|
||||
console.error('[OpencodeClient] Global SSE stream error (will retry):', error, 'isNetworkError:', isNetworkError);
|
||||
if (isNetworkError) {
|
||||
console.error('[OpencodeClient] Network error detected - OpenCode server may have crashed or disconnected!');
|
||||
}
|
||||
this.notifyGlobalSseError(error);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const streamingPartQueue: QueuedStreamingPart[] = [];
|
||||
let streamingFlushScheduled = false;
|
||||
let streamingFlushRafId: number | null = null;
|
||||
let streamingFlushTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
const STREAMING_FLUSH_TIMEOUT_MS = 50;
|
||||
const STREAMING_FLUSH_TIMEOUT_MS = 8;
|
||||
const STREAMING_QUEUE_HARD_LIMIT = 3000;
|
||||
|
||||
type StreamingPartImmediateHandler = (
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface SessionContextUsage {
|
||||
export const DEFAULT_MESSAGE_LIMIT = 200;
|
||||
|
||||
/** Timeout after which a session stuck in 'busy' or 'retry' with no SSE events is force-reset to idle. */
|
||||
export const STUCK_SESSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
export const STUCK_SESSION_TIMEOUT_MS = 25 * 60 * 1000; // 25 minutes
|
||||
|
||||
export const MEMORY_CONSTANTS = {
|
||||
MAX_SESSIONS: 3,
|
||||
|
||||
Binary file not shown.
@@ -5825,7 +5825,9 @@ function setupProxy(app) {
|
||||
|
||||
const requestHeaders = {
|
||||
...(typeof req.headers.accept === 'string' ? { accept: req.headers.accept } : { accept: 'text/event-stream' }),
|
||||
'cache-control': 'no-cache',
|
||||
'cache-control': 'no-cache, no-store, must-revalidate',
|
||||
'pragma': 'no-cache',
|
||||
'expires': '0',
|
||||
connection: 'keep-alive',
|
||||
...(authHeaders.Authorization ? { Authorization: authHeaders.Authorization } : {}),
|
||||
};
|
||||
@@ -5859,7 +5861,7 @@ function setupProxy(app) {
|
||||
idleTimer = setTimeout(() => {
|
||||
endedBy = 'idle-timeout';
|
||||
controller.abort();
|
||||
}, 5 * 60 * 1000);
|
||||
}, 30 * 60 * 1000); // 30 minutes
|
||||
};
|
||||
|
||||
const onClientClose = () => {
|
||||
@@ -5901,7 +5903,9 @@ function setupProxy(app) {
|
||||
const upstreamContentType = upstreamResponse.headers.get('content-type') || 'text/event-stream';
|
||||
res.status(upstreamResponse.status);
|
||||
res.setHeader('content-type', upstreamContentType);
|
||||
res.setHeader('cache-control', 'no-cache');
|
||||
res.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
|
||||
res.setHeader('pragma', 'no-cache');
|
||||
res.setHeader('expires', '0');
|
||||
res.setHeader('connection', 'keep-alive');
|
||||
res.setHeader('x-accel-buffering', 'no');
|
||||
res.setHeader('x-content-type-options', 'nosniff');
|
||||
|
||||
Reference in New Issue
Block a user