|
|
|
@ -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(); |
|
|
|
|