fix: add thread safety for concurrent state modifications
- Add stateLock to synchronize all getState/setState read-modify-write operations across UI thread and AgentLoop background threads - Synchronize onInput, onPaste, addMessage, appendToStreamingMessage, finishStreamingMessage, completeLastToolCall, setThinking, runAgent - Extract addMessageInternal() to avoid double-locking - Harden extractToolSummary with explicit indexOf checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -71,6 +71,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
private final Runnable onExit;
|
private final Runnable onExit;
|
||||||
|
|
||||||
// --- 内部状态 ---
|
// --- 内部状态 ---
|
||||||
|
private final Object stateLock = new Object(); // 保护 getState/setState 的读-改-写操作
|
||||||
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 = "";
|
||||||
@@ -441,6 +442,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInput(String input, Key key) {
|
public void onInput(String input, Key key) {
|
||||||
|
synchronized (stateLock) {
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
|
|
||||||
// Ctrl+D: 退出
|
// Ctrl+D: 退出
|
||||||
@@ -453,7 +455,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
if (key.ctrl() && "c".equals(input)) {
|
if (key.ctrl() && "c".equals(input)) {
|
||||||
if (agentRunning.get()) {
|
if (agentRunning.get()) {
|
||||||
// TODO: 中断 Agent 运行
|
// TODO: 中断 Agent 运行
|
||||||
addMessage(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW));
|
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
|
||||||
} else {
|
} else {
|
||||||
setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
|
setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
|
||||||
}
|
}
|
||||||
@@ -506,14 +508,17 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
setState(new TuiState(s.inputText + input, s.messages, s.scrollOffset, false, ""));
|
setState(new TuiState(s.inputText + input, s.messages, s.scrollOffset, false, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPaste(String text) {
|
public void onPaste(String text) {
|
||||||
|
synchronized (stateLock) {
|
||||||
if (agentRunning.get() || text == null || text.isEmpty()) return;
|
if (agentRunning.get() || text == null || text.isEmpty()) return;
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
abandonHistoryPreview();
|
abandonHistoryPreview();
|
||||||
setState(new TuiState(s.inputText + text, s.messages, s.scrollOffset, false, ""));
|
setState(new TuiState(s.inputText + text, s.messages, s.scrollOffset, false, ""));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 处理权限确认输入 */
|
/** 处理权限确认输入 */
|
||||||
private void handlePermissionInput(String input, Key key, TuiState s) {
|
private void handlePermissionInput(String input, Key key, TuiState s) {
|
||||||
@@ -605,9 +610,11 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
addMessage(new SystemMsg("Error: " + e.getMessage(), Color.BRIGHT_RED));
|
addMessage(new SystemMsg("Error: " + e.getMessage(), Color.BRIGHT_RED));
|
||||||
} finally {
|
} finally {
|
||||||
agentRunning.set(false);
|
agentRunning.set(false);
|
||||||
|
synchronized (stateLock) {
|
||||||
TuiState cs = getState();
|
TuiState cs = getState();
|
||||||
setState(new TuiState(cs.inputText, cs.messages, 0, false, ""));
|
setState(new TuiState(cs.inputText, cs.messages, 0, false, ""));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +622,13 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
|
|
||||||
/** 添加一条消息 */
|
/** 添加一条消息 */
|
||||||
public void addMessage(UIMessage msg) {
|
public void addMessage(UIMessage msg) {
|
||||||
TuiState s = getState();
|
synchronized (stateLock) {
|
||||||
|
addMessageInternal(msg, getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 内部添加消息(调用方需持有 stateLock) */
|
||||||
|
private void addMessageInternal(UIMessage msg, TuiState s) {
|
||||||
List<UIMessage> newMsgs = new ArrayList<>(s.messages);
|
List<UIMessage> newMsgs = new ArrayList<>(s.messages);
|
||||||
newMsgs.add(msg);
|
newMsgs.add(msg);
|
||||||
setState(new TuiState(s.inputText, Collections.unmodifiableList(newMsgs),
|
setState(new TuiState(s.inputText, Collections.unmodifiableList(newMsgs),
|
||||||
@@ -624,6 +637,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
|
|
||||||
/** 追加 token 到当前流式助手消息 */
|
/** 追加 token 到当前流式助手消息 */
|
||||||
private void appendToStreamingMessage(String token) {
|
private void appendToStreamingMessage(String token) {
|
||||||
|
synchronized (stateLock) {
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
List<UIMessage> msgs = new ArrayList<>(s.messages);
|
List<UIMessage> msgs = new ArrayList<>(s.messages);
|
||||||
|
|
||||||
@@ -637,9 +651,11 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs),
|
setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs),
|
||||||
0, s.thinking, s.thinkingText));
|
0, s.thinking, s.thinkingText));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 完成当前流式消息(公开给 JinkReplSession 使用) */
|
/** 完成当前流式消息(公开给 JinkReplSession 使用) */
|
||||||
public void finishStreamingMessage() {
|
public void finishStreamingMessage() {
|
||||||
|
synchronized (stateLock) {
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
List<UIMessage> msgs = new ArrayList<>(s.messages);
|
List<UIMessage> msgs = new ArrayList<>(s.messages);
|
||||||
|
|
||||||
@@ -649,9 +665,11 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
0, s.thinking, s.thinkingText));
|
0, s.thinking, s.thinkingText));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 更新最后一个工具调用消息的结果 */
|
/** 更新最后一个工具调用消息的结果 */
|
||||||
public void completeLastToolCall(String result) {
|
public void completeLastToolCall(String result) {
|
||||||
|
synchronized (stateLock) {
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
List<UIMessage> msgs = new ArrayList<>(s.messages);
|
List<UIMessage> msgs = new ArrayList<>(s.messages);
|
||||||
|
|
||||||
@@ -665,6 +683,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs),
|
setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs),
|
||||||
s.scrollOffset, s.thinking, s.thinkingText));
|
s.scrollOffset, s.thinking, s.thinkingText));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 设置权限确认回调 */
|
/** 设置权限确认回调 */
|
||||||
public void requestPermission(Consumer<String> callback) {
|
public void requestPermission(Consumer<String> callback) {
|
||||||
@@ -673,9 +692,11 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
|
|
||||||
/** 设置 thinking 状态 */
|
/** 设置 thinking 状态 */
|
||||||
public void setThinking(boolean thinking, String text) {
|
public void setThinking(boolean thinking, String text) {
|
||||||
|
synchronized (stateLock) {
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, thinking, text));
|
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, thinking, text));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 设置首次用户输入回调 */
|
/** 设置首次用户输入回调 */
|
||||||
public void setOnFirstUserInput(Consumer<String> callback) {
|
public void setOnFirstUserInput(Consumer<String> callback) {
|
||||||
@@ -747,11 +768,13 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
String[] keys = {"command", "file_path", "pattern", "query", "url"};
|
String[] keys = {"command", "file_path", "pattern", "query", "url"};
|
||||||
for (String key : keys) {
|
for (String key : keys) {
|
||||||
String search = "\"" + key + "\"";
|
String search = "\"" + key + "\"";
|
||||||
if (args.contains(search)) {
|
|
||||||
int start = args.indexOf(search);
|
int start = args.indexOf(search);
|
||||||
int valStart = args.indexOf("\"", start + search.length()) + 1;
|
if (start < 0) continue;
|
||||||
|
int colonPos = args.indexOf("\"", start + search.length());
|
||||||
|
if (colonPos < 0) continue;
|
||||||
|
int valStart = colonPos + 1;
|
||||||
int valEnd = args.indexOf("\"", valStart);
|
int valEnd = args.indexOf("\"", valStart);
|
||||||
if (valStart > 0 && valEnd > valStart) {
|
if (valEnd < 0 || valEnd <= valStart) continue;
|
||||||
String val = args.substring(valStart, Math.min(valEnd, valStart + 60));
|
String val = args.substring(valStart, Math.min(valEnd, valStart + 60));
|
||||||
return switch (key) {
|
return switch (key) {
|
||||||
case "command" -> "$ " + val;
|
case "command" -> "$ " + val;
|
||||||
@@ -760,8 +783,6 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
default -> val;
|
default -> val;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user