fix: improve terminal history rendering
This commit is contained in:
parent
21f206559e
commit
9a83f92dd9
@ -7,7 +7,7 @@ TERMINAL_TIMEOUT = 300
|
|||||||
TERMINAL_OUTPUT_WAIT = 5
|
TERMINAL_OUTPUT_WAIT = 5
|
||||||
TERMINAL_SNAPSHOT_DEFAULT_LINES = 50
|
TERMINAL_SNAPSHOT_DEFAULT_LINES = 50
|
||||||
TERMINAL_SNAPSHOT_MAX_LINES = 200
|
TERMINAL_SNAPSHOT_MAX_LINES = 200
|
||||||
TERMINAL_SNAPSHOT_MAX_CHARS = 60000
|
TERMINAL_SNAPSHOT_MAX_CHARS = 6000000
|
||||||
TERMINAL_INPUT_MAX_CHARS = 20000
|
TERMINAL_INPUT_MAX_CHARS = 20000
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@ -536,33 +536,46 @@ class PersistentTerminal:
|
|||||||
output = f"[输出已截断,显示最后{self.display_size}字符]\n{output}"
|
output = f"[输出已截断,显示最后{self.display_size}字符]\n{output}"
|
||||||
return 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:
|
include_all_lines = last_n_lines is None or last_n_lines <= 0
|
||||||
last_n_lines = 1
|
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:
|
for event_type, _, data in self.io_history:
|
||||||
if event_type == 'input':
|
if event_type == 'input':
|
||||||
# 显示输入命令,保持与终端监控一致
|
# 显示输入命令并加上ANSI颜色,保持与实时终端一致
|
||||||
combined_lines.append(f"➜ {data.rstrip()}" if data.strip() else "➜")
|
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:
|
else:
|
||||||
cleaned = data.replace('\r', '')
|
cleaned = data.replace('\r', '')
|
||||||
# 按行拆分输出,保留空行
|
segments.append(cleaned)
|
||||||
segments = cleaned.splitlines()
|
|
||||||
if cleaned.endswith('\n'):
|
|
||||||
segments.append('')
|
|
||||||
combined_lines.extend(segments if segments else [''])
|
|
||||||
|
|
||||||
if combined_lines:
|
full_text = ''.join(segments)
|
||||||
selected_lines = combined_lines[-last_n_lines:]
|
if full_text:
|
||||||
output_text = '\n'.join(selected_lines)
|
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:
|
else:
|
||||||
selected_lines = []
|
|
||||||
output_text = ''
|
output_text = ''
|
||||||
|
lines_requested = 0
|
||||||
|
total_lines = 0
|
||||||
|
|
||||||
truncated = False
|
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:]
|
output_text = output_text[-max_chars:]
|
||||||
truncated = True
|
truncated = True
|
||||||
|
|
||||||
@ -572,11 +585,14 @@ class PersistentTerminal:
|
|||||||
else:
|
else:
|
||||||
lines_returned = 0
|
lines_returned = 0
|
||||||
|
|
||||||
|
if include_all_lines:
|
||||||
|
lines_requested = total_lines
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"session": self.session_name,
|
"session": self.session_name,
|
||||||
"output": output_text,
|
"output": output_text,
|
||||||
"lines_requested": last_n_lines,
|
"lines_requested": lines_requested,
|
||||||
"lines_returned": lines_returned,
|
"lines_returned": lines_returned,
|
||||||
"truncated": truncated,
|
"truncated": truncated,
|
||||||
"is_interactive": self.is_interactive,
|
"is_interactive": self.is_interactive,
|
||||||
|
|||||||
@ -416,11 +416,27 @@ class TerminalManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
terminal = self.terminals[target_session]
|
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(
|
snapshot = terminal.get_snapshot(
|
||||||
last_n_lines or self.default_snapshot_lines,
|
snapshot_lines,
|
||||||
self.max_snapshot_chars
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -466,11 +482,16 @@ class TerminalManager:
|
|||||||
"existing_sessions": list(self.terminals.keys())
|
"existing_sessions": list(self.terminals.keys())
|
||||||
}
|
}
|
||||||
|
|
||||||
line_limit = lines if lines is not None else self.default_snapshot_lines
|
if lines is None:
|
||||||
line_limit = max(1, min(line_limit, self.max_snapshot_lines))
|
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))
|
char_limit = max(100, min(max_chars if max_chars else self.max_snapshot_chars, self.max_snapshot_chars))
|
||||||
|
|
||||||
terminal = self.terminals[target_session]
|
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 = terminal.get_snapshot(line_limit, char_limit)
|
||||||
snapshot.update({
|
snapshot.update({
|
||||||
"line_limit": line_limit,
|
"line_limit": line_limit,
|
||||||
|
|||||||
@ -99,8 +99,8 @@
|
|||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 22px;
|
padding: 22px 12px;
|
||||||
gap: 22px;
|
gap: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.terminal-wrapper {
|
.terminal-wrapper {
|
||||||
@ -152,24 +152,38 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
#terminal {
|
#terminal {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0 28px 0 36px;
|
margin: 0 14px 0 18px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
#terminal .xterm {
|
||||||
|
flex: 1;
|
||||||
|
height: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
#terminal .xterm-viewport {
|
#terminal .xterm-viewport {
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
padding-bottom: 0;
|
overflow-y: auto !important;
|
||||||
}
|
padding: 0 !important;
|
||||||
#terminal .xterm-scroll-area {
|
margin: 0 !important;
|
||||||
margin-bottom: 32px;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
#terminal .xterm-scroll-area,
|
||||||
#terminal .xterm-rows {
|
#terminal .xterm-rows {
|
||||||
padding-bottom: 20px;
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#terminal .xterm-viewport::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
@ -179,6 +193,10 @@
|
|||||||
border: 1px solid var(--claude-border);
|
border: 1px solid var(--claude-border);
|
||||||
box-shadow: 0 18px 40px rgba(61, 57, 41, 0.1);
|
box-shadow: 0 18px 40px rgba(61, 57, 41, 0.1);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.sidebar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
.info-section {
|
.info-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
@ -751,7 +769,7 @@
|
|||||||
pendingHistory[name] = needsHistory;
|
pendingHistory[name] = needsHistory;
|
||||||
renderCurrentSessionLog();
|
renderCurrentSessionLog();
|
||||||
if (needsHistory && socket) {
|
if (needsHistory && socket) {
|
||||||
socket.emit('get_terminal_output', { session: name });
|
socket.emit('get_terminal_output', { session: name, lines: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user