fix: 修复代码审查发现的线程安全和边界问题
- render() 中对 volatile 字段取快照,避免跨线程竞争 - 光标位置 clamp 到 >= 0,防止小终端越界 - spinner 线程使用独立 spinnerLock 保护启停操作 - PrintStream 使用 try-with-resources 避免资源泄漏 - 终端高度 < 20 时隐藏标题框腾出空间 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -72,6 +72,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
|
|
||||||
// --- 内部状态 ---
|
// --- 内部状态 ---
|
||||||
private final Object stateLock = new Object(); // 保护 getState/setState 的读-改-写操作
|
private final Object stateLock = new Object(); // 保护 getState/setState 的读-改-写操作
|
||||||
|
private final Object spinnerLock = new Object(); // 保护 spinner 线程的启停
|
||||||
private final List<String> inputHistory = new ArrayList<>();
|
private final List<String> inputHistory = new ArrayList<>();
|
||||||
private int historyIndex = -1;
|
private int historyIndex = -1;
|
||||||
private String savedInput = "";
|
private String savedInput = "";
|
||||||
@@ -122,29 +123,41 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
int w = getColumns();
|
int w = getColumns();
|
||||||
int h = getRows();
|
int h = getRows();
|
||||||
|
|
||||||
|
// 快照 volatile 字段(避免 render 过程中被其他线程修改)
|
||||||
|
final List<String> snapAskOptions;
|
||||||
|
final int snapAskSelected;
|
||||||
|
final boolean snapAskInputMode;
|
||||||
|
final boolean snapHasCallback;
|
||||||
|
synchronized (stateLock) {
|
||||||
|
snapAskOptions = askOptions != null ? List.copyOf(askOptions) : null;
|
||||||
|
snapAskSelected = askSelectedIndex;
|
||||||
|
snapAskInputMode = askInputMode;
|
||||||
|
snapHasCallback = permissionCallback != null;
|
||||||
|
}
|
||||||
|
|
||||||
// 计算输入区行数
|
// 计算输入区行数
|
||||||
int inputLineCount = 1;
|
int inputLineCount = 1;
|
||||||
String lastLine = s.inputText;
|
String lastLine = s.inputText;
|
||||||
if (askOptions != null && !askOptions.isEmpty() && permissionCallback != null) {
|
if (snapAskOptions != null && !snapAskOptions.isEmpty() && snapHasCallback) {
|
||||||
// AskUser 模式:选项数 + 提示行
|
// AskUser 模式:选项数 + 提示行
|
||||||
inputLineCount = askOptions.size() + 1;
|
inputLineCount = snapAskOptions.size() + 1;
|
||||||
} else if (!s.inputText.isEmpty()) {
|
} else if (!s.inputText.isEmpty()) {
|
||||||
String[] inputLines = s.inputText.split("\n", -1);
|
String[] inputLines = s.inputText.split("\n", -1);
|
||||||
inputLineCount = inputLines.length;
|
inputLineCount = inputLines.length;
|
||||||
lastLine = inputLines[inputLines.length - 1];
|
lastLine = inputLines[inputLines.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 光标定位
|
// 光标定位(clamp 到 >= 0 防止小终端越界)
|
||||||
if (askOptions != null && !askOptions.isEmpty() && permissionCallback != null) {
|
if (snapAskOptions != null && !snapAskOptions.isEmpty() && snapHasCallback) {
|
||||||
// AskUser 模式:隐藏光标(选项列表模式不需要)
|
if (snapAskInputMode) {
|
||||||
if (askInputMode) {
|
int askCursorRow = h - 2 - (snapAskOptions.size() - snapAskSelected);
|
||||||
int askCursorRow = h - 2 - (askOptions.size() - askSelectedIndex);
|
setCursorPosition(Math.max(0, askCursorRow), 7 + StringWidth.width(s.inputText));
|
||||||
setCursorPosition(askCursorRow, 7 + StringWidth.width(s.inputText));
|
|
||||||
} else {
|
} else {
|
||||||
setCursorPosition(h - 2 - (askOptions.size() - askSelectedIndex), 6);
|
int askCursorRow = h - 2 - (snapAskOptions.size() - snapAskSelected);
|
||||||
|
setCursorPosition(Math.max(0, askCursorRow), 6);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int cursorRow = h - 3;
|
int cursorRow = Math.max(0, h - 3);
|
||||||
int cursorCol = 1 + PROMPT_WIDTH + StringWidth.width(lastLine);
|
int cursorCol = 1 + PROMPT_WIDTH + StringWidth.width(lastLine);
|
||||||
setCursorPosition(cursorRow, cursorCol);
|
setCursorPosition(cursorRow, cursorCol);
|
||||||
}
|
}
|
||||||
@@ -152,18 +165,28 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
int headerHeight = 8; // 6 content rows + 2 border lines
|
int headerHeight = 8; // 6 content rows + 2 border lines
|
||||||
int bottomHeight = 4 + inputLineCount;
|
int bottomHeight = 4 + inputLineCount;
|
||||||
int messagePaddingTop = 1;
|
int messagePaddingTop = 1;
|
||||||
int maxMessageLines = h - headerHeight - bottomHeight - messagePaddingTop;
|
int maxMessageLines = Math.max(1, h - headerHeight - bottomHeight - messagePaddingTop);
|
||||||
|
|
||||||
return Box.of(
|
// 终端高度太小时,隐藏标题框以腾出消息空间
|
||||||
headerBox(w),
|
boolean showHeader = h >= 20;
|
||||||
messagesArea(s, maxMessageLines),
|
if (!showHeader) {
|
||||||
Spacer.create(),
|
maxMessageLines = Math.max(1, h - bottomHeight - messagePaddingTop);
|
||||||
statusBar(w, h),
|
}
|
||||||
separator(w),
|
|
||||||
inputArea(s, w),
|
List<Renderable> layout = new ArrayList<>();
|
||||||
separator(w),
|
if (showHeader) {
|
||||||
shortcutBar(w)
|
layout.add(headerBox(w));
|
||||||
).flexDirection(FlexDirection.COLUMN).width(w).height(h);
|
}
|
||||||
|
layout.add(messagesArea(s, maxMessageLines));
|
||||||
|
layout.add(Spacer.create());
|
||||||
|
layout.add(statusBar(w, h));
|
||||||
|
layout.add(separator(w));
|
||||||
|
layout.add(inputArea(s, w));
|
||||||
|
layout.add(separator(w));
|
||||||
|
layout.add(shortcutBar(w));
|
||||||
|
|
||||||
|
return Box.of(layout.toArray(new Renderable[0]))
|
||||||
|
.flexDirection(FlexDirection.COLUMN).width(w).height(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 标题框 — 保留原始 ASCII Logo 样式(双列布局) */
|
/** 标题框 — 保留原始 ASCII Logo 样式(双列布局) */
|
||||||
@@ -721,7 +744,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
addMessage(new UserMsg(text));
|
addMessage(new UserMsg(text));
|
||||||
// 捕获命令输出到 ByteArrayOutputStream
|
// 捕获命令输出到 ByteArrayOutputStream
|
||||||
var baos = new ByteArrayOutputStream();
|
var baos = new ByteArrayOutputStream();
|
||||||
var capturedOut = new PrintStream(baos, true, StandardCharsets.UTF_8);
|
try (var capturedOut = new PrintStream(baos, true, StandardCharsets.UTF_8)) {
|
||||||
CommandContext cmdCtx = new CommandContext(agentLoop, toolRegistry, commandRegistry,
|
CommandContext cmdCtx = new CommandContext(agentLoop, toolRegistry, commandRegistry,
|
||||||
capturedOut, () -> {
|
capturedOut, () -> {
|
||||||
if (onExit != null) onExit.run();
|
if (onExit != null) onExit.run();
|
||||||
@@ -738,6 +761,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
if (!output.isEmpty()) {
|
if (!output.isEmpty()) {
|
||||||
addMessage(new CommandOutputMsg(output.toString()));
|
addMessage(new CommandOutputMsg(output.toString()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
setState(new TuiState("", getState().messages, 0, false, ""));
|
setState(new TuiState("", getState().messages, 0, false, ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -788,13 +812,14 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
|
|
||||||
/** 启动思考动画 */
|
/** 启动思考动画 */
|
||||||
private void startSpinner() {
|
private void startSpinner() {
|
||||||
|
synchronized (spinnerLock) {
|
||||||
|
stopSpinnerInternal();
|
||||||
spinnerFrame = 0;
|
spinnerFrame = 0;
|
||||||
Thread t = Thread.startVirtualThread(() -> {
|
Thread t = Thread.startVirtualThread(() -> {
|
||||||
try {
|
try {
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
Thread.sleep(120);
|
Thread.sleep(120);
|
||||||
spinnerFrame++;
|
spinnerFrame++;
|
||||||
// 触发重绘:读取当前状态并重新设置(内容不变,但 spinner 帧已更新)
|
|
||||||
synchronized (stateLock) {
|
synchronized (stateLock) {
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, s.thinking, s.thinkingText));
|
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, s.thinking, s.thinkingText));
|
||||||
@@ -804,9 +829,16 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
});
|
});
|
||||||
spinnerThread = t;
|
spinnerThread = t;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 停止思考动画 */
|
/** 停止思考动画 */
|
||||||
private void stopSpinner() {
|
private void stopSpinner() {
|
||||||
|
synchronized (spinnerLock) {
|
||||||
|
stopSpinnerInternal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopSpinnerInternal() {
|
||||||
Thread t = spinnerThread;
|
Thread t = spinnerThread;
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
t.interrupt();
|
t.interrupt();
|
||||||
|
|||||||
Reference in New Issue
Block a user