Refactor: Improve console error/log display in CLI (#486)
This commit is contained in:
parent
fb1d13d600
commit
7eaf850489
|
@ -5,8 +5,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||||
import { Box, DOMElement, measureElement, Static, Text } from 'ink';
|
import {
|
||||||
import { StreamingState, type HistoryItem } from './types.js';
|
Box,
|
||||||
|
DOMElement,
|
||||||
|
measureElement,
|
||||||
|
Static,
|
||||||
|
Text,
|
||||||
|
useInput,
|
||||||
|
type Key as InkKeyType,
|
||||||
|
} from 'ink';
|
||||||
|
import {
|
||||||
|
StreamingState,
|
||||||
|
type HistoryItem,
|
||||||
|
ConsoleMessageItem,
|
||||||
|
MessageType,
|
||||||
|
} from './types.js';
|
||||||
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
||||||
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
||||||
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
||||||
|
@ -25,11 +38,11 @@ import { Help } from './components/Help.js';
|
||||||
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
||||||
import { LoadedSettings } from '../config/settings.js';
|
import { LoadedSettings } from '../config/settings.js';
|
||||||
import { Tips } from './components/Tips.js';
|
import { Tips } from './components/Tips.js';
|
||||||
import { ConsoleOutput } from './components/ConsolePatcher.js';
|
import { useConsolePatcher } from './components/ConsolePatcher.js';
|
||||||
|
import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js';
|
||||||
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
|
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
|
||||||
import { useHistory } from './hooks/useHistoryManager.js';
|
import { useHistory } from './hooks/useHistoryManager.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { MessageType } from './types.js';
|
|
||||||
import { getErrorMessage, type Config } from '@gemini-code/server';
|
import { getErrorMessage, type Config } from '@gemini-code/server';
|
||||||
import { useLogger } from './hooks/useLogger.js';
|
import { useLogger } from './hooks/useLogger.js';
|
||||||
|
|
||||||
|
@ -61,6 +74,32 @@ export const App = ({
|
||||||
const [corgiMode, setCorgiMode] = useState(false);
|
const [corgiMode, setCorgiMode] = useState(false);
|
||||||
const [shellModeActive, setShellModeActive] = useState(false);
|
const [shellModeActive, setShellModeActive] = useState(false);
|
||||||
|
|
||||||
|
const [consoleMessages, setConsoleMessages] = useState<ConsoleMessageItem[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const errorCount = useMemo(
|
||||||
|
() => consoleMessages.filter((msg) => msg.type === 'error').length,
|
||||||
|
[consoleMessages],
|
||||||
|
);
|
||||||
|
useInput((input: string, key: InkKeyType) => {
|
||||||
|
// Check for Ctrl+D key press
|
||||||
|
if (key.ctrl && (input === 'd' || input === 'D')) {
|
||||||
|
setShowErrorDetails((prev) => !prev);
|
||||||
|
refreshStatic();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNewMessage = useCallback((message: ConsoleMessageItem) => {
|
||||||
|
setConsoleMessages((prevMessages) => [...prevMessages, message]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useConsolePatcher({
|
||||||
|
onNewMessage: handleNewMessage,
|
||||||
|
debugMode: config.getDebugMode(),
|
||||||
|
});
|
||||||
|
|
||||||
const toggleCorgiMode = useCallback(() => {
|
const toggleCorgiMode = useCallback(() => {
|
||||||
setCorgiMode((prev) => !prev);
|
setCorgiMode((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -72,7 +111,6 @@ export const App = ({
|
||||||
handleThemeHighlight,
|
handleThemeHighlight,
|
||||||
} = useThemeCommand(settings, setThemeError);
|
} = useThemeCommand(settings, setThemeError);
|
||||||
|
|
||||||
// useEffect to initialize geminiMdFileCount from config when config is ready
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config) {
|
if (config) {
|
||||||
setGeminiMdFileCount(config.getGeminiMdFileCount());
|
setGeminiMdFileCount(config.getGeminiMdFileCount());
|
||||||
|
@ -186,12 +224,11 @@ export const App = ({
|
||||||
|
|
||||||
const handleClearScreen = useCallback(() => {
|
const handleClearScreen = useCallback(() => {
|
||||||
clearItems();
|
clearItems();
|
||||||
|
setConsoleMessages([]);
|
||||||
console.clear();
|
console.clear();
|
||||||
refreshStatic();
|
refreshStatic();
|
||||||
}, [clearItems, refreshStatic]);
|
}, [clearItems, refreshStatic]);
|
||||||
|
|
||||||
// --- Render Logic ---
|
|
||||||
|
|
||||||
const { rows: terminalHeight } = useTerminalSize();
|
const { rows: terminalHeight } = useTerminalSize();
|
||||||
const mainControlsRef = useRef<DOMElement>(null);
|
const mainControlsRef = useRef<DOMElement>(null);
|
||||||
const pendingHistoryItemRef = useRef<DOMElement>(null);
|
const pendingHistoryItemRef = useRef<DOMElement>(null);
|
||||||
|
@ -201,7 +238,7 @@ export const App = ({
|
||||||
const fullFooterMeasurement = measureElement(mainControlsRef.current);
|
const fullFooterMeasurement = measureElement(mainControlsRef.current);
|
||||||
setFooterHeight(fullFooterMeasurement.height);
|
setFooterHeight(fullFooterMeasurement.height);
|
||||||
}
|
}
|
||||||
}, [terminalHeight]);
|
}, [terminalHeight, consoleMessages, showErrorDetails]);
|
||||||
|
|
||||||
const availableTerminalHeight = useMemo(() => {
|
const availableTerminalHeight = useMemo(() => {
|
||||||
const staticExtraHeight = /* margins and padding */ 3;
|
const staticExtraHeight = /* margins and padding */ 3;
|
||||||
|
@ -232,6 +269,13 @@ export const App = ({
|
||||||
}
|
}
|
||||||
}, [streamingState, refreshStatic, staticNeedsRefresh]);
|
}, [streamingState, refreshStatic, staticNeedsRefresh]);
|
||||||
|
|
||||||
|
const filteredConsoleMessages = useMemo(() => {
|
||||||
|
if (config.getDebugMode()) {
|
||||||
|
return consoleMessages;
|
||||||
|
}
|
||||||
|
return consoleMessages.filter((msg) => msg.type !== 'debug');
|
||||||
|
}, [consoleMessages, config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1} width="90%">
|
<Box flexDirection="column" marginBottom={1} width="90%">
|
||||||
{/*
|
{/*
|
||||||
|
@ -339,6 +383,11 @@ export const App = ({
|
||||||
{shellModeActive && <ShellModeIndicator />}
|
{shellModeActive && <ShellModeIndicator />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{showErrorDetails && (
|
||||||
|
<DetailedMessagesDisplay messages={filteredConsoleMessages} />
|
||||||
|
)}
|
||||||
|
|
||||||
{isInputActive && (
|
{isInputActive && (
|
||||||
<InputPrompt
|
<InputPrompt
|
||||||
widthFraction={0.9}
|
widthFraction={0.9}
|
||||||
|
@ -392,8 +441,9 @@ export const App = ({
|
||||||
debugMessage={debugMessage}
|
debugMessage={debugMessage}
|
||||||
cliVersion={cliVersion}
|
cliVersion={cliVersion}
|
||||||
corgiMode={corgiMode}
|
corgiMode={corgiMode}
|
||||||
|
errorCount={errorCount}
|
||||||
|
showErrorDetails={showErrorDetails}
|
||||||
/>
|
/>
|
||||||
<ConsoleOutput debugMode={config.getDebugMode()} />
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,27 +4,19 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, Key } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
import { ConsoleMessageItem } from '../types.js';
|
||||||
|
|
||||||
interface ConsoleMessage {
|
interface UseConsolePatcherParams {
|
||||||
id: Key;
|
onNewMessage: (message: Omit<ConsoleMessageItem, 'id'>) => void;
|
||||||
type: 'log' | 'warn' | 'error' | 'debug';
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using a module-level counter for unique IDs.
|
|
||||||
// This ensures IDs are unique across messages.
|
|
||||||
let messageIdCounter = 0;
|
|
||||||
|
|
||||||
interface ConsoleOutputProps {
|
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
|
export const useConsolePatcher = ({
|
||||||
const [messages, setMessages] = useState<ConsoleMessage[]>([]);
|
onNewMessage,
|
||||||
|
debugMode,
|
||||||
|
}: UseConsolePatcherParams): void => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const originalConsoleLog = console.log;
|
const originalConsoleLog = console.log;
|
||||||
const originalConsoleWarn = console.warn;
|
const originalConsoleWarn = console.warn;
|
||||||
|
@ -32,25 +24,30 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
|
||||||
const originalConsoleDebug = console.debug;
|
const originalConsoleDebug = console.debug;
|
||||||
|
|
||||||
const formatArgs = (args: unknown[]): string => util.format(...args);
|
const formatArgs = (args: unknown[]): string => util.format(...args);
|
||||||
const addMessage = (
|
|
||||||
type: 'log' | 'warn' | 'error' | 'debug',
|
|
||||||
args: unknown[],
|
|
||||||
) => {
|
|
||||||
setMessages((prevMessages) => [
|
|
||||||
...prevMessages,
|
|
||||||
{
|
|
||||||
id: `console-msg-${messageIdCounter++}`,
|
|
||||||
type,
|
|
||||||
content: formatArgs(args),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// It's patching time
|
const patchConsoleMethod =
|
||||||
console.log = (...args: unknown[]) => addMessage('log', args);
|
(
|
||||||
console.warn = (...args: unknown[]) => addMessage('warn', args);
|
type: 'log' | 'warn' | 'error' | 'debug',
|
||||||
console.error = (...args: unknown[]) => addMessage('error', args);
|
originalMethod: (...args: unknown[]) => void,
|
||||||
console.debug = (...args: unknown[]) => addMessage('debug', args);
|
) =>
|
||||||
|
(...args: unknown[]) => {
|
||||||
|
if (debugMode) {
|
||||||
|
originalMethod.apply(console, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, if it's not a debug message or debugMode is on, pass to onNewMessage
|
||||||
|
if (type !== 'debug' || debugMode) {
|
||||||
|
onNewMessage({
|
||||||
|
type,
|
||||||
|
content: formatArgs(args),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log = patchConsoleMethod('log', originalConsoleLog);
|
||||||
|
console.warn = patchConsoleMethod('warn', originalConsoleWarn);
|
||||||
|
console.error = patchConsoleMethod('error', originalConsoleError);
|
||||||
|
console.debug = patchConsoleMethod('debug', originalConsoleDebug);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log = originalConsoleLog;
|
console.log = originalConsoleLog;
|
||||||
|
@ -58,46 +55,5 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
|
||||||
console.error = originalConsoleError;
|
console.error = originalConsoleError;
|
||||||
console.debug = originalConsoleDebug;
|
console.debug = originalConsoleDebug;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [onNewMessage, debugMode]);
|
||||||
|
|
||||||
return (
|
|
||||||
<Box flexDirection="column">
|
|
||||||
{messages.map((msg) => {
|
|
||||||
if (msg.type === 'debug' && !debugMode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const textProps: { color?: string } = {};
|
|
||||||
let prefix = '';
|
|
||||||
|
|
||||||
switch (msg.type) {
|
|
||||||
case 'warn':
|
|
||||||
textProps.color = 'yellow';
|
|
||||||
prefix = 'WARN: ';
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
textProps.color = 'red';
|
|
||||||
prefix = 'ERROR: ';
|
|
||||||
break;
|
|
||||||
case 'debug':
|
|
||||||
textProps.color = 'gray';
|
|
||||||
prefix = 'DEBUG: ';
|
|
||||||
break;
|
|
||||||
case 'log':
|
|
||||||
default:
|
|
||||||
prefix = 'LOG: ';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box key={msg.id}>
|
|
||||||
<Text {...textProps}>
|
|
||||||
{prefix}
|
|
||||||
{msg.content}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { Colors } from '../colors.js';
|
||||||
|
|
||||||
|
interface ConsoleSummaryDisplayProps {
|
||||||
|
errorCount: number;
|
||||||
|
// logCount is not currently in the plan to be displayed in summary
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConsoleSummaryDisplay: React.FC<ConsoleSummaryDisplayProps> = ({
|
||||||
|
errorCount,
|
||||||
|
}) => {
|
||||||
|
if (errorCount === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorIcon = '\u2716'; // Heavy multiplication x (✖)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{errorCount > 0 && (
|
||||||
|
<Text color={Colors.AccentRed}>
|
||||||
|
{errorIcon} {errorCount} error{errorCount > 1 ? 's' : ''}{' '}
|
||||||
|
<Text color={Colors.SubtleComment}>(CTRL-D for details)</Text>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { Colors } from '../colors.js';
|
||||||
|
import { ConsoleMessageItem } from '../types.js';
|
||||||
|
|
||||||
|
interface DetailedMessagesDisplayProps {
|
||||||
|
messages: ConsoleMessageItem[];
|
||||||
|
// debugMode is not needed here if App.tsx filters debug messages before passing them.
|
||||||
|
// If DetailedMessagesDisplay should handle filtering, add debugMode prop.
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DetailedMessagesDisplay: React.FC<
|
||||||
|
DetailedMessagesDisplayProps
|
||||||
|
> = ({ messages }) => {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return null; // Don't render anything if there are no messages
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
marginTop={1}
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={Colors.SubtleComment}
|
||||||
|
paddingX={1}
|
||||||
|
>
|
||||||
|
<Box marginBottom={1}>
|
||||||
|
<Text bold color={Colors.Foreground}>
|
||||||
|
Debug Console{' '}
|
||||||
|
<Text color={Colors.SubtleComment}>(CTRL-D to close)</Text>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{messages.map((msg, index) => {
|
||||||
|
let textColor = Colors.Foreground;
|
||||||
|
let icon = '\u2139'; // Information source (ℹ)
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'warn':
|
||||||
|
textColor = Colors.AccentYellow;
|
||||||
|
icon = '\u26A0'; // Warning sign (⚠)
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
textColor = Colors.AccentRed;
|
||||||
|
icon = '\u2716'; // Heavy multiplication x (✖)
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
textColor = Colors.SubtleComment; // Or Colors.Gray
|
||||||
|
icon = '\u1F50D'; // Left-pointing magnifying glass (????)
|
||||||
|
break;
|
||||||
|
case 'log':
|
||||||
|
default:
|
||||||
|
// Default textColor and icon are already set
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={index} flexDirection="row">
|
||||||
|
<Text color={textColor}>{icon} </Text>
|
||||||
|
<Text color={textColor} wrap="wrap">
|
||||||
|
{msg.content}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,6 +8,7 @@ import React from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { shortenPath, tildeifyPath, Config } from '@gemini-code/server';
|
import { shortenPath, tildeifyPath, Config } from '@gemini-code/server';
|
||||||
|
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
config: Config;
|
config: Config;
|
||||||
|
@ -15,6 +16,8 @@ interface FooterProps {
|
||||||
debugMessage: string;
|
debugMessage: string;
|
||||||
cliVersion: string;
|
cliVersion: string;
|
||||||
corgiMode: boolean;
|
corgiMode: boolean;
|
||||||
|
errorCount: number;
|
||||||
|
showErrorDetails: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Footer: React.FC<FooterProps> = ({
|
export const Footer: React.FC<FooterProps> = ({
|
||||||
|
@ -23,8 +26,10 @@ export const Footer: React.FC<FooterProps> = ({
|
||||||
debugMessage,
|
debugMessage,
|
||||||
cliVersion,
|
cliVersion,
|
||||||
corgiMode,
|
corgiMode,
|
||||||
|
errorCount,
|
||||||
|
showErrorDetails,
|
||||||
}) => (
|
}) => (
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1} justifyContent="space-between" width="100%">
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={Colors.LightBlue}>
|
<Text color={Colors.LightBlue}>
|
||||||
{shortenPath(tildeifyPath(config.getTargetDir()), 70)}
|
{shortenPath(tildeifyPath(config.getTargetDir()), 70)}
|
||||||
|
@ -56,8 +61,8 @@ export const Footer: React.FC<FooterProps> = ({
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Right Section: Gemini Label */}
|
{/* Right Section: Gemini Label and Console Summary */}
|
||||||
<Box>
|
<Box alignItems="center">
|
||||||
<Text color={Colors.AccentBlue}> {config.getModel()} </Text>
|
<Text color={Colors.AccentBlue}> {config.getModel()} </Text>
|
||||||
<Text color={Colors.SubtleComment}>| CLI {cliVersion} </Text>
|
<Text color={Colors.SubtleComment}>| CLI {cliVersion} </Text>
|
||||||
{corgiMode && (
|
{corgiMode && (
|
||||||
|
@ -70,6 +75,12 @@ export const Footer: React.FC<FooterProps> = ({
|
||||||
<Text color={Colors.AccentRed}>▼ </Text>
|
<Text color={Colors.AccentRed}>▼ </Text>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
{!showErrorDetails && errorCount > 0 && (
|
||||||
|
<Box>
|
||||||
|
<Text color={Colors.SubtleComment}>| </Text>
|
||||||
|
<ConsoleSummaryDisplay errorCount={errorCount} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -119,3 +119,8 @@ export interface Message {
|
||||||
content: string; // Renamed from text for clarity in this context
|
content: string; // Renamed from text for clarity in this context
|
||||||
timestamp: Date; // For consistency, though addItem might use its own timestamping
|
timestamp: Date; // For consistency, though addItem might use its own timestamping
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConsoleMessageItem {
|
||||||
|
type: 'log' | 'warn' | 'error' | 'debug';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue