Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc9d0ce9

Browse files
committed
Pre-release 0.44.152
1 parent8770b9e commitc9d0ce9

File tree

26 files changed

+494
-103
lines changed

26 files changed

+494
-103
lines changed

‎Core/Sources/ChatService/ChatService.swift‎

Lines changed: 91 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ struct ToolCallRequest {
4949
letcompletion:(AnyJSONRPCResponse)->Void
5050
}
5151

52+
structConversationTurnTrackingState{
53+
varturnParentMap:[String:String]=[:] // Maps subturn ID to parent turn ID
54+
varvalidConversationIds:Set<String>=[] // Tracks all valid conversation IDs including subagents
55+
56+
mutatingfunc reset(){
57+
turnParentMap.removeAll()
58+
validConversationIds.removeAll()
59+
}
60+
}
61+
5262
publicfinalclassChatService:ChatServiceType,ObservableObject{
5363

5464
publicvarmemory:ContextAwareAutoManagedChatMemory
@@ -69,6 +79,9 @@ public final class ChatService: ChatServiceType, ObservableObject {
6979
privatevarlastUserRequest:ConversationRequest?
7080
privatevarisRestored:Bool=false
7181
privatevarpendingToolCallRequests:[String:ToolCallRequest]=[:]
82+
// Workaround: toolConfirmation request does not have parent turnId
83+
privatevarconversationTurnTracking=ConversationTurnTrackingState()
84+
7285
init(provider:anyConversationServiceProvider,
7386
memory:ContextAwareAutoManagedChatMemory=ContextAwareAutoManagedChatMemory(),
7487
conversationProgressHandler:ConversationProgressHandler=ConversationProgressHandlerImpl.shared,
@@ -136,7 +149,15 @@ public final class ChatService: ChatServiceType, ObservableObject {
136149

137150
privatefunc subscribeToClientToolConfirmationEvent(){
138151
ClientToolHandlerImpl.shared.onClientToolConfirmationEvent.sink(receiveValue:{[weak self](request, completion)in
139-
guardlet params= request.params, params.conversationId==self?.conversationIdelse{return}
152+
guardlet params= request.paramselse{return}
153+
154+
// Check if this conversationId is valid (main conversation or subagent conversation)
155+
guardlet validIds=self?.conversationTurnTracking.validConversationIds, validIds.contains(params.conversationId)else{
156+
return
157+
}
158+
159+
letparentTurnId=self?.conversationTurnTracking.turnParentMap[params.turnId]
160+
140161
leteditAgentRounds:[AgentRound]=[
141162
AgentRound(roundId: params.roundId,
142163
reply:"",
@@ -145,7 +166,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
145166
]
146167
)
147168
]
148-
self?.appendToolCallHistory(turnId: params.turnId, editAgentRounds: editAgentRounds)
169+
self?.appendToolCallHistory(turnId: params.turnId, editAgentRounds: editAgentRounds, parentTurnId: parentTurnId)
149170
self?.pendingToolCallRequests[params.toolCallId]=ToolCallRequest(
150171
requestId: request.id,
151172
turnId: params.turnId,
@@ -157,7 +178,13 @@ public final class ChatService: ChatServiceType, ObservableObject {
157178

158179
privatefunc subscribeToClientToolInvokeEvent(){
159180
ClientToolHandlerImpl.shared.onClientToolInvokeEvent.sink(receiveValue:{[weak self](request, completion)in
160-
guardlet params= request.params, params.conversationId==self?.conversationIdelse{return}
181+
guardlet params= request.paramselse{return}
182+
183+
// Check if this conversationId is valid (main conversation or subagent conversation)
184+
guardlet validIds=self?.conversationTurnTracking.validConversationIds, validIds.contains(params.conversationId)else{
185+
return
186+
}
187+
161188
guardlet copilotTool=CopilotToolRegistry.shared.getTool(name: params.name)else{
162189
completion(AnyJSONRPCResponse(id: request.id,
163190
result:JSONValue.array([
@@ -173,11 +200,11 @@ public final class ChatService: ChatServiceType, ObservableObject {
173200
return
174201
}
175202

176-
copilotTool.invokeTool(request, completion: completion, contextProvider:self)
203+
_=copilotTool.invokeTool(request, completion: completion, contextProvider:self)
177204
}).store(in:&cancellables)
178205
}
179206

180-
func appendToolCallHistory(turnId:String, editAgentRounds:[AgentRound], fileEdits:[FileEdit]=[]){
207+
func appendToolCallHistory(turnId:String, editAgentRounds:[AgentRound], fileEdits:[FileEdit]=[], parentTurnId:String?=nil){
181208
letchatTabId=self.chatTabInfo.id
182209
Task{
183210
letturnStatus:ChatMessage.TurnStatus?={
@@ -196,6 +223,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
196223
assistantMessageWithId: turnId,
197224
chatTabID: chatTabId,
198225
editAgentRounds: editAgentRounds,
226+
parentTurnId: parentTurnId,
199227
fileEdits: fileEdits,
200228
turnStatus: turnStatus
201229
)
@@ -229,75 +257,64 @@ public final class ChatService: ChatServiceType, ObservableObject {
229257
self.isRestored=true
230258
}
231259

260+
/// Updates the status of a tool call (accepted, cancelled, etc.) and notifies the server
261+
///
262+
/// This method handles two key responsibilities:
263+
/// 1. Sends confirmation response back to the server when user accepts/cancels
264+
/// 2. Updates the tool call status in chat history UI (including subagent tool calls)
232265
publicfunc updateToolCallStatus(toolCallId:String, status:AgentToolCall.ToolCallStatus, payload:Any?=nil){
233-
// Send the tool call result back to the server
234-
iflet toolCallRequest=self.pendingToolCallRequests[toolCallId], status==.accepted || status==.cancelled{
266+
// Capture the pending request info before removing it from the dictionary
267+
lettoolCallRequest=self.pendingToolCallRequests[toolCallId]
268+
269+
// Step 1: Send confirmation response to server (for accept/cancel actions only)
270+
iflet toolCallRequest= toolCallRequest, status==.accepted || status==.cancelled{
235271
self.pendingToolCallRequests.removeValue(forKey: toolCallId)
236-
lettoolResult=LanguageModelToolConfirmationResult(
237-
result: status==.accepted?.Accept:.Dismiss
238-
)
239-
letjsonResult=try?JSONEncoder().encode(toolResult)
240-
letjsonValue=(try?JSONDecoder().decode(JSONValue.self, from: jsonResult??Data()))??JSONValue.null
241-
toolCallRequest.completion(
242-
AnyJSONRPCResponse(
243-
id: toolCallRequest.requestId,
244-
result:JSONValue.array([
245-
jsonValue,
246-
JSONValue.null
247-
])
248-
)
249-
)
272+
sendToolConfirmationResponse(toolCallRequest, accepted: status==.accepted)
250273
}
251274

252-
// Update the tool call status inthechat history
275+
//Step 2:Update the tool call status in chat history UI
253276
Task{
254-
guardlet lastMessage=await memory.history.last, lastMessage.role==.assistantelse{
277+
guardlet targetMessage=awaitToolCallStatusUpdater.findMessageContainingToolCall(
278+
toolCallRequest,
279+
conversationTurnTracking: conversationTurnTracking,
280+
history:await memory.history
281+
)else{
255282
return
256283
}
257-
258-
varupdatedAgentRounds:[AgentRound]=[]
259-
foriin0..<lastMessage.editAgentRounds.count{
260-
if lastMessage.editAgentRounds[i].toolCalls==nil{
261-
continue
262-
}
263-
forjin0..<lastMessage.editAgentRounds[i].toolCalls!.count{
264-
if lastMessage.editAgentRounds[i].toolCalls![j].id== toolCallId{
265-
updatedAgentRounds.append(
266-
AgentRound(roundId: lastMessage.editAgentRounds[i].roundId,
267-
reply:"",
268-
toolCalls:[
269-
AgentToolCall(id: toolCallId,
270-
name: lastMessage.editAgentRounds[i].toolCalls![j].name,
271-
status: status)
272-
]
273-
)
274-
)
275-
break
276-
}
277-
}
278-
if !updatedAgentRounds.isEmpty{
279-
break
280-
}
281-
}
282-
283-
if !updatedAgentRounds.isEmpty{
284-
letmessage=ChatMessage(
285-
id: lastMessage.id,
286-
chatTabID: lastMessage.chatTabID,
287-
clsTurnID: lastMessage.clsTurnID,
288-
role:.assistant,
289-
content:"",
290-
references:[],
291-
steps:[],
292-
editAgentRounds: updatedAgentRounds,
293-
turnStatus:.inProgress
284+
285+
// Search for the tool call in main rounds or subagent rounds
286+
iflet updatedRound=ToolCallStatusUpdater.findAndUpdateToolCall(
287+
toolCallId: toolCallId,
288+
newStatus: status,
289+
in: targetMessage.editAgentRounds
290+
){
291+
letmessage=ToolCallStatusUpdater.createMessageUpdate(
292+
targetMessage: targetMessage,
293+
updatedRound: updatedRound
294294
)
295-
296-
awaitself.memory.appendMessage(message)
295+
await memory.appendMessage(message)
297296
}
298297
}
299298
}
300299

300+
// MARK: - Helper Methods for Tool Call Status Updates
301+
302+
/// Sends the confirmation response (accept/dismiss) back to the server
303+
privatefunc sendToolConfirmationResponse(_ request:ToolCallRequest, accepted:Bool){
304+
lettoolResult=LanguageModelToolConfirmationResult(
305+
result: accepted?.Accept:.Dismiss
306+
)
307+
letjsonResult=try?JSONEncoder().encode(toolResult)
308+
letjsonValue=(try?JSONDecoder().decode(JSONValue.self, from: jsonResult??Data()))??JSONValue.null
309+
310+
request.completion(
311+
AnyJSONRPCResponse(
312+
id: request.requestId,
313+
result:JSONValue.array([jsonValue,JSONValue.null])
314+
)
315+
)
316+
}
317+
301318
publicenumChatServiceError:Error,LocalizedError{
302319
case conflictingImageFormats(String)
303320

@@ -676,9 +693,18 @@ public final class ChatService: ChatServiceType, ObservableObject {
676693
if progress.parentTurnId==nil{
677694
conversationId= progress.conversationId
678695
}
696+
697+
// Track all valid conversation IDs for the current turn (main conversation + its subturns)
698+
conversationTurnTracking.validConversationIds.insert(progress.conversationId)
699+
679700
letturnId= progress.turnId
680701
letparentTurnId= progress.parentTurnId
681702

703+
// Track parent-subturn relationship
704+
iflet parentTurnId= parentTurnId{
705+
conversationTurnTracking.turnParentMap[turnId]= parentTurnId
706+
}
707+
682708
Task{
683709
ifvar lastUserMessage=await memory.history.last(where:{ $0.role==.user}){
684710

@@ -870,6 +896,9 @@ public final class ChatService: ChatServiceType, ObservableObject {
870896
activeRequestId=nil
871897
isReceivingMessage=false
872898
requestType=nil
899+
900+
// Clear turn tracking data
901+
conversationTurnTracking.reset()
873902

874903
// cancel all pending tool call requests
875904
for(_, request)in pendingToolCallRequests{
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import ChatAPIService
2+
import ConversationServiceProvider
3+
import Foundation
4+
5+
/// Helper methods for updating tool call status in chat history
6+
/// Handles both main turn tool calls and subagent tool calls
7+
structToolCallStatusUpdater{
8+
/// Finds the message containing the tool call, handling both main turns and subturns
9+
staticfunc findMessageContainingToolCall(
10+
_ toolCallRequest:ToolCallRequest?,
11+
conversationTurnTracking:ConversationTurnTrackingState,
12+
history:[ChatMessage]
13+
)async->ChatMessage?{
14+
guardlet request= toolCallRequestelse{returnnil}
15+
16+
// If this is a subturn, find the parent turn; otherwise use the request's turnId
17+
letturnIdToFind= conversationTurnTracking.turnParentMap[request.turnId]?? request.turnId
18+
19+
return history.first(where:{ $0.id== turnIdToFind && $0.role==.assistant})
20+
}
21+
22+
/// Searches for a tool call in agent rounds (including nested subagent rounds) and creates an update
23+
///
24+
/// Note: Parent turns can have multiple sequential subturns, but they don't appear simultaneously.
25+
/// Subturns are merged into the parent's last round's subAgentRounds array by ChatMemory.
26+
staticfunc findAndUpdateToolCall(
27+
toolCallId:String,
28+
newStatus:AgentToolCall.ToolCallStatus,
29+
in agentRounds:[AgentRound]
30+
)->AgentRound?{
31+
// First, search in main rounds (for regular tool calls)
32+
forroundin agentRounds{
33+
iflet toolCalls= round.toolCalls{
34+
fortoolCallin toolCallswhere toolCall.id== toolCallId{
35+
returnAgentRound(
36+
roundId: round.roundId,
37+
reply:"",
38+
toolCalls:[
39+
AgentToolCall(
40+
id: toolCallId,
41+
name: toolCall.name,
42+
status: newStatus
43+
),
44+
]
45+
)
46+
}
47+
}
48+
}
49+
50+
// If not found in main rounds, search in subagent rounds (for subturn tool calls)
51+
// Subturns are nested under the parent round's subAgentRounds
52+
forroundin agentRounds{
53+
guardlet subAgentRounds= round.subAgentRoundselse{continue}
54+
55+
forsubRoundin subAgentRounds{
56+
guardlet toolCalls= subRound.toolCallselse{continue}
57+
58+
fortoolCallin toolCallswhere toolCall.id== toolCallId{
59+
// Create an update that will be merged into the parent's subAgentRounds
60+
// ChatMemory.appendMessage will handle the merging logic
61+
letsubagentRound=AgentRound(
62+
roundId: subRound.roundId,
63+
reply:"",
64+
toolCalls:[
65+
AgentToolCall(
66+
id: toolCallId,
67+
name: toolCall.name,
68+
status: newStatus
69+
),
70+
]
71+
)
72+
returnAgentRound(
73+
roundId: round.roundId,
74+
reply:"",
75+
toolCalls:[],
76+
subAgentRounds:[subagentRound]
77+
)
78+
}
79+
}
80+
}
81+
82+
returnnil
83+
}
84+
85+
/// Creates a message update with the new tool call status
86+
staticfunc createMessageUpdate(
87+
targetMessage:ChatMessage,
88+
updatedRound:AgentRound
89+
)->ChatMessage{
90+
returnChatMessage(
91+
id: targetMessage.id,
92+
chatTabID: targetMessage.chatTabID,
93+
clsTurnID: targetMessage.clsTurnID,
94+
role:.assistant,
95+
content:"",
96+
references:[],
97+
steps:[],
98+
editAgentRounds:[updatedRound],
99+
turnStatus:.inProgress
100+
)
101+
}
102+
}

‎Core/Sources/ConversationTab/ModeAndModelPicker/ModePicker/AgentModeButton.swift‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ import Persist
44
import SharedUIComponents
55
import SwiftUI
66

7+
// MARK: - Custom NSButton that accepts clicks anywhere within its bounds
8+
classClickThroughButton:NSButton{
9+
overridefunc hitTest(_ point:NSPoint)->NSView?{
10+
// If the point is within our bounds, return self (the button)
11+
// This ensures clicks on subviews are handled by the button
12+
ifself.bounds.contains(point){
13+
returnself
14+
}
15+
return super.hitTest(point)
16+
}
17+
}
18+
719
// MARK: - Agent Mode Button
820

921
structAgentModeButton:NSViewRepresentable{
@@ -33,7 +45,7 @@ struct AgentModeButton: NSViewRepresentable {
3345
letcontainerView=NSView()
3446
containerView.translatesAutoresizingMaskIntoConstraints=false
3547

36-
letbutton=NSButton()
48+
letbutton=ClickThroughButton()
3749
button.title=""
3850
button.bezelStyle=.inline
3951
button.setButtonType(.momentaryPushIn)

‎Core/Sources/ConversationTab/ModeAndModelPicker/ModePicker/AgentModeButtonMenuItem.swift‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class AgentModeButtonMenuItem: NSView {
101101
isSelected:Bool,
102102
menuHasSelection:Bool,
103103
fontScale:Double=1.0,
104+
fixedWidth:CGFloat?=nil,
104105
onSelect:@escaping()->Void,
105106
onEdit:(()->Void)?=nil,
106107
onDelete:(()->Void)?=nil
@@ -114,8 +115,8 @@ class AgentModeButtonMenuItem: NSView {
114115
self.onEdit= onEdit
115116
self.onDelete= onDelete
116117

117-
//Calculate dynamic widthbased on content
118-
letcalculatedWidth=Self.calculateWidth(
118+
//Use fixed widthif provided, otherwise calculate dynamically
119+
letcalculatedWidth=fixedWidth??Self.calculateMenuItemWidth(
119120
name: name,
120121
hasIcon: iconName!=nil,
121122
isSelected: isSelected,
@@ -133,7 +134,7 @@ class AgentModeButtonMenuItem: NSView {
133134
fatalError("init(coder:) has not been implemented")
134135
}
135136

136-
privatestaticfunccalculateWidth(
137+
staticfunccalculateMenuItemWidth(
137138
name:String,
138139
hasIcon:Bool,
139140
isSelected:Bool,

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp