fix: improve terminal history rendering

This commit is contained in:
JOJO 2025-11-21 19:06:12 +08:00
parent 21f206559e
commit 9a83f92dd9
4 changed files with 87 additions and 32 deletions

View File

@ -7,7 +7,7 @@ TERMINAL_TIMEOUT = 300
TERMINAL_OUTPUT_WAIT = 5
TERMINAL_SNAPSHOT_DEFAULT_LINES = 50
TERMINAL_SNAPSHOT_MAX_LINES = 200
TERMINAL_SNAPSHOT_MAX_CHARS = 60000
TERMINAL_SNAPSHOT_MAX_CHARS = 6000000
TERMINAL_INPUT_MAX_CHARS = 20000
__all__ = [

View File

@ -536,33 +536,46 @@ class PersistentTerminal:
output = f"[输出已截断,显示最后{self.display_size}字符]\n{output}"
return output
def get_snapshot(self, last_n_lines: int, max_chars: int) -> Dict:
def get_snapshot(self, last_n_lines: Optional[int], max_chars: Optional[int]) -> Dict:
"""获取终端快照,包含按顺序排列的输入/输出"""
if last_n_lines <= 0:
last_n_lines = 1
include_all_lines = last_n_lines is None or last_n_lines <= 0
if not include_all_lines:
last_n_lines = max(1, last_n_lines)
combined_lines: List[str] = []
segments: List[str] = []
reset = "\x1b[0m"
command_color = "\x1b[1;32m"
for event_type, _, data in self.io_history:
if event_type == 'input':
# 显示输入命令,保持与终端监控一致
combined_lines.append(f"{data.rstrip()}" if data.strip() else "")
# 显示输入命令并加上ANSI颜色保持与实时终端一致
input_text = data.rstrip('\n')
decorated = f"{command_color}{input_text}{reset}" if input_text.strip() else f"{command_color}{reset}"
if not decorated.endswith('\n'):
decorated += '\n'
segments.append(decorated)
else:
cleaned = data.replace('\r', '')
# 按行拆分输出,保留空行
segments = cleaned.splitlines()
if cleaned.endswith('\n'):
segments.append('')
combined_lines.extend(segments if segments else [''])
segments.append(cleaned)
if combined_lines:
selected_lines = combined_lines[-last_n_lines:]
output_text = '\n'.join(selected_lines)
full_text = ''.join(segments)
if full_text:
line_chunks = full_text.splitlines(keepends=True)
total_lines = len(line_chunks)
if include_all_lines:
selected_lines = line_chunks
lines_requested = total_lines
else:
count = min(last_n_lines, total_lines)
selected_lines = line_chunks[-count:] if count else []
lines_requested = last_n_lines
output_text = ''.join(selected_lines)
else:
selected_lines = []
output_text = ''
lines_requested = 0
total_lines = 0
truncated = False
if len(output_text) > max_chars:
if max_chars is not None and max_chars > 0 and len(output_text) > max_chars:
output_text = output_text[-max_chars:]
truncated = True
@ -572,11 +585,14 @@ class PersistentTerminal:
else:
lines_returned = 0
if include_all_lines:
lines_requested = total_lines
return {
"success": True,
"session": self.session_name,
"output": output_text,
"lines_requested": last_n_lines,
"lines_requested": lines_requested,
"lines_returned": lines_returned,
"truncated": truncated,
"is_interactive": self.is_interactive,

View File

@ -416,11 +416,27 @@ class TerminalManager:
}
terminal = self.terminals[target_session]
requested_lines = last_n_lines
if requested_lines is None:
snapshot_lines = self.default_snapshot_lines
elif requested_lines <= 0:
snapshot_lines = None
else:
snapshot_lines = min(requested_lines, self.max_snapshot_lines)
snapshot_char_limit = self.max_snapshot_chars if snapshot_lines is not None else None
snapshot = terminal.get_snapshot(
last_n_lines or self.default_snapshot_lines,
self.max_snapshot_chars
snapshot_lines,
snapshot_char_limit
)
output = snapshot.get("output") if snapshot.get("success") else terminal.get_output(last_n_lines)
fallback_lines = requested_lines
if fallback_lines is None:
fallback_lines = self.default_snapshot_lines
elif fallback_lines <= 0:
fallback_lines = 0
output = snapshot.get("output") if snapshot.get("success") else terminal.get_output(fallback_lines)
return {
"success": True,
@ -466,11 +482,16 @@ class TerminalManager:
"existing_sessions": list(self.terminals.keys())
}
line_limit = lines if lines is not None else self.default_snapshot_lines
line_limit = max(1, min(line_limit, self.max_snapshot_lines))
if lines is None:
line_limit = self.default_snapshot_lines
elif lines <= 0:
line_limit = None
else:
line_limit = max(1, min(lines, self.max_snapshot_lines))
char_limit = max(100, min(max_chars if max_chars else self.max_snapshot_chars, self.max_snapshot_chars))
terminal = self.terminals[target_session]
char_limit = None if line_limit is None else char_limit
snapshot = terminal.get_snapshot(line_limit, char_limit)
snapshot.update({
"line_limit": line_limit,

View File

@ -99,8 +99,8 @@
.main-content {
flex: 1;
display: flex;
padding: 22px;
gap: 22px;
padding: 22px 12px;
gap: 20px;
overflow: hidden;
}
.terminal-wrapper {
@ -152,24 +152,38 @@
display: flex;
align-items: stretch;
justify-content: stretch;
min-height: 0;
}
#terminal {
flex: 1;
height: 100%;
margin: 0 28px 0 36px;
margin: 0 14px 0 18px;
background: #ffffff;
color: var(--claude-text);
overflow: hidden;
display: flex;
min-height: 0;
}
#terminal .xterm {
flex: 1;
height: 100% !important;
width: 100% !important;
min-height: 0;
}
#terminal .xterm-viewport {
overflow-x: hidden !important;
padding-bottom: 0;
}
#terminal .xterm-scroll-area {
margin-bottom: 32px;
overflow-y: auto !important;
padding: 0 !important;
margin: 0 !important;
scrollbar-width: none;
}
#terminal .xterm-scroll-area,
#terminal .xterm-rows {
padding-bottom: 20px;
margin: 0 !important;
padding: 0 !important;
}
#terminal .xterm-viewport::-webkit-scrollbar {
display: none;
}
.sidebar {
width: 300px;
@ -179,6 +193,10 @@
border: 1px solid var(--claude-border);
box-shadow: 0 18px 40px rgba(61, 57, 41, 0.1);
overflow-y: auto;
scrollbar-width: none;
}
.sidebar::-webkit-scrollbar {
display: none;
}
.info-section {
margin-bottom: 20px;
@ -751,7 +769,7 @@
pendingHistory[name] = needsHistory;
renderCurrentSessionLog();
if (needsHistory && socket) {
socket.emit('get_terminal_output', { session: name });
socket.emit('get_terminal_output', { session: name, lines: 0 });
}
}