Skip to content

Commit c21fc8b

Browse files
authored
Merge pull request #839 from github/release/0.48.172
Pre-release 0.48.172
2 parents 48da723 + aebdeb7 commit c21fc8b

62 files changed

Lines changed: 1319 additions & 310 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/auto-create-release-pr.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ jobs:
4747
run: |
4848
gh pr merge "${{ github.ref_name }}" \
4949
--auto \
50+
--merge \
5051
--delete-branch

CommunicationBridge/ServiceDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ actor ExtensionServiceLauncher {
175175
return configuration
176176
}()
177177
) { app, error in
178-
if let error = error {
178+
if error != nil {
179179
continuation.resume(returning: nil)
180180
} else {
181181
continuation.resume(returning: app)

Copilot for Xcode/App.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
144144

145145
// Start cleanup in background without waiting
146146
Task {
147-
let quitTask = Task {
147+
_ = Task {
148148
let service = try? getService()
149149
try? await service?.quitService()
150150
}

Core/Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ let package = Package(
7575
dependencies: [
7676
"SuggestionWidget",
7777
"SuggestionService",
78+
"SuggestionInjector",
7879
"ChatService",
7980
"PromptToCodeService",
8081
"ConversationTab",
@@ -123,6 +124,7 @@ let package = Package(
123124
"Client",
124125
"LaunchAgentManager",
125126
"GitHubCopilotViewModel",
127+
"UpdateChecker",
126128
.product(name: "SuggestionProvider", package: "Tool"),
127129
.product(name: "Toast", package: "Tool"),
128130
.product(name: "SharedUIComponents", package: "Tool"),
@@ -202,6 +204,7 @@ let package = Package(
202204
name: "ConversationTab",
203205
dependencies: [
204206
"ChatService",
207+
"GitHubCopilotViewModel",
205208
.product(name: "SharedUIComponents", package: "Tool"),
206209
.product(name: "ChatAPIService", package: "Tool"),
207210
.product(name: "Logger", package: "Tool"),
@@ -225,6 +228,7 @@ let package = Package(
225228
"ConversationTab",
226229
"GitHubCopilotViewModel",
227230
"PersistMiddleware",
231+
.product(name: "CGEventOverride", package: "CGEventOverride"),
228232
.product(name: "GitHubCopilotService", package: "Tool"),
229233
.product(name: "Toast", package: "Tool"),
230234
.product(name: "UserDefaultsObserver", package: "Tool"),

Core/Sources/ChatService/ChatService.swift

Lines changed: 250 additions & 56 deletions
Large diffs are not rendered by default.

Core/Sources/ChatService/ToolCalls/AutoApproval/AutoApprovalScope.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

33
public typealias ConversationID = String
44

5-
public enum AutoApprovalScope: Hashable {
5+
public enum AutoApprovalScope: Hashable, Sendable {
66
case session(ConversationID)
77
/// Applies to all workspaces. Persisted in `UserDefaults.autoApproval`.
88
case global

Core/Sources/ConversationTab/Chat.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public struct DisplayedChatMessage: Equatable {
3030
public var suggestedTitle: String? = nil
3131
public var errorMessages: [String] = []
3232
public var steps: [ConversationProgressStep] = []
33+
public var thinking: [MessageThinking] = []
3334
public var editAgentRounds: [AgentRound] = []
3435
public var parentTurnId: String? = nil
3536
public var panelMessages: [CopilotShowMessageParams] = []
@@ -50,6 +51,7 @@ public struct DisplayedChatMessage: Equatable {
5051
suggestedTitle: String? = nil,
5152
errorMessages: [String] = [],
5253
steps: [ConversationProgressStep] = [],
54+
thinking: [MessageThinking] = [],
5355
editAgentRounds: [AgentRound] = [],
5456
parentTurnId: String? = nil,
5557
panelMessages: [CopilotShowMessageParams] = [],
@@ -69,6 +71,7 @@ public struct DisplayedChatMessage: Equatable {
6971
self.suggestedTitle = suggestedTitle
7072
self.errorMessages = errorMessages
7173
self.steps = steps
74+
self.thinking = thinking
7275
self.editAgentRounds = editAgentRounds
7376
self.parentTurnId = parentTurnId
7477
self.panelMessages = panelMessages
@@ -1067,6 +1070,7 @@ struct Chat {
10671070
suggestedTitle: message.suggestedTitle,
10681071
errorMessages: message.errorMessages,
10691072
steps: message.steps,
1073+
thinking: message.thinking,
10701074
editAgentRounds: message.editAgentRounds,
10711075
parentTurnId: message.parentTurnId,
10721076
panelMessages: message.panelMessages,
@@ -1230,7 +1234,7 @@ struct Chat {
12301234
return .none
12311235

12321236
// MARK: - Code Review
1233-
case let .codeReview(.request(group)):
1237+
case .codeReview(.request(_)):
12341238
return .run { send in
12351239
await send(.discardCheckPoint)
12361240
}

Core/Sources/ConversationTab/ChatPanel.swift

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ private let r: Double = 4
2323
public struct ChatPanel: View {
2424
@Perception.Bindable var chat: StoreOf<Chat>
2525
@Namespace var inputAreaNamespace
26+
@ObservedObject private var rateLimitNotifier = RateLimitNotifierImpl.shared
2627

2728
public var body: some View {
2829
WithPerceptionTracking {
@@ -55,12 +56,20 @@ public struct ChatPanel: View {
5556
}
5657
}
5758

59+
if let warning = rateLimitNotifier.currentWarning {
60+
RateLimitWarningBanner(message: warning.message) {
61+
rateLimitNotifier.dismissWarning()
62+
}
63+
.scaledPadding(.horizontal, 24)
64+
.scaledPadding(.vertical, 8)
65+
}
66+
5867
if chat.fileEditMap.count > 0 {
5968
WorkingSetView(chat: chat)
6069
.dimWithExitEditMode(chat)
6170
.scaledPadding(.horizontal, 24)
6271
}
63-
72+
6473
ChatPanelInputArea(chat: chat, r: r, editorMode: .input)
6574
.dimWithExitEditMode(chat)
6675
.scaledPadding(.horizontal, 16)
@@ -135,6 +144,36 @@ private struct ListHeightPreferenceKey: PreferenceKey {
135144
}
136145
}
137146

147+
private struct ScrollViewConfigurator: NSViewRepresentable {
148+
let configure: (NSScrollView) -> Void
149+
150+
final class Coordinator {
151+
var didConfigure = false
152+
}
153+
154+
func makeCoordinator() -> Coordinator { Coordinator() }
155+
156+
func makeNSView(context: Context) -> NSView {
157+
let view = NSView()
158+
applyOnce(view: view, coordinator: context.coordinator)
159+
return view
160+
}
161+
162+
func updateNSView(_ nsView: NSView, context: Context) {
163+
applyOnce(view: nsView, coordinator: context.coordinator)
164+
}
165+
166+
private func applyOnce(view: NSView, coordinator: Coordinator) {
167+
guard !coordinator.didConfigure else { return }
168+
DispatchQueue.main.async {
169+
guard !coordinator.didConfigure,
170+
let scrollView = view.enclosingScrollView else { return }
171+
coordinator.didConfigure = true
172+
configure(scrollView)
173+
}
174+
}
175+
}
176+
138177
struct ChatPanelMessages: View {
139178
let chat: StoreOf<Chat>
140179
@State var cancellable = Set<AnyCancellable>()
@@ -154,17 +193,34 @@ struct ChatPanelMessages: View {
154193
WithPerceptionTracking {
155194
ScrollViewReader { proxy in
156195
GeometryReader { listGeo in
157-
List {
158-
Group {
196+
ScrollView(.vertical, showsIndicators: true) {
197+
// VStack with a flexible trailing Spacer absorbs empty space when
198+
// content is shorter than the viewport, so content stays naturally
199+
// top-aligned. When content grows past the viewport, the Spacer
200+
// collapses to its minLength and the VStack overflows the
201+
// ScrollView's content area as expected. This avoids the List's
202+
// remembered-bottom-anchor behavior that pushed earlier content up
203+
// whenever a child view's height changed.
204+
VStack(alignment: .leading, spacing: 0) {
205+
ScrollViewConfigurator { scrollView in
206+
scrollView.scrollerStyle = .overlay
207+
scrollView.verticalScroller?.scrollerStyle = .overlay
208+
scrollView.autohidesScrollers = true
209+
}
210+
.frame(width: 0, height: 0)
211+
212+
Color.clear
213+
.frame(height: 1)
214+
.id(topID)
159215

160216
ChatHistory(chat: chat)
161217
.fixedSize(horizontal: false, vertical: true)
162218

163219
ExtraSpacingInResponding(chat: chat)
164220

165-
Spacer(minLength: 12)
221+
Color.clear
222+
.frame(height: 12)
166223
.id(bottomID)
167-
.listRowInsets(EdgeInsets())
168224
.onAppear {
169225
isBottomHidden = false
170226
if !didScrollToBottomOnAppearOnce {
@@ -182,14 +238,16 @@ struct ChatPanelMessages: View {
182238
value: offset
183239
)
184240
})
241+
242+
Spacer(minLength: 0)
185243
}
186-
.listRowSeparator(.hidden)
187-
}
188-
.listStyle(.plain)
189-
.scaledPadding(.leading, 8)
190-
.listRowBackground(EmptyView())
191-
.modify { view in
192-
view.scrollContentBackground(.hidden)
244+
.frame(
245+
minWidth: 0,
246+
maxWidth: .infinity,
247+
minHeight: listGeo.size.height,
248+
alignment: .topLeading
249+
)
250+
.scaledPadding(.horizontal, 16)
193251
}
194252
.coordinateSpace(name: scrollSpace)
195253
.preference(

Core/Sources/ConversationTab/Views/BotMessage.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct BotMessage: View {
2121
var followUp: ConversationFollowUp? { message.followUp }
2222
var errorMessages: [String] { message.errorMessages }
2323
var steps: [ConversationProgressStep] { message.steps }
24+
var thinking: [MessageThinking] { message.thinking }
2425
var editAgentRounds: [AgentRound] { message.editAgentRounds }
2526
var panelMessages: [CopilotShowMessageParams] { message.panelMessages }
2627
var codeReviewRound: CodeReviewRound? { message.codeReviewRound }
@@ -90,21 +91,28 @@ struct BotMessage: View {
9091
// progress step
9192
if steps.count > 0 {
9293
ProgressStep(steps: steps)
93-
94+
9495
}
95-
96+
97+
ForEach(Array(thinking.enumerated()), id: \.offset) { index, entry in
98+
ThinkingView(
99+
thinking: entry,
100+
isStreaming: index == thinking.count - 1 && isThinkingStreaming()
101+
)
102+
}
103+
96104
if !panelMessages.isEmpty {
97105
WithPerceptionTracking {
98106
ForEach(panelMessages.indices, id: \.self) { index in
99107
FunctionMessage(text: panelMessages[index].message, chat: chat)
100108
}
101109
}
102110
}
103-
111+
104112
if editAgentRounds.count > 0 {
105-
ProgressAgentRound(rounds: editAgentRounds, chat: chat)
113+
ProgressAgentRound(rounds: editAgentRounds, chat: chat, isStreaming: isThinkingStreaming())
106114
}
107-
115+
108116
if !text.isEmpty {
109117
Group{
110118
ThemedMarkdownText(text: text, chat: chat)
@@ -241,6 +249,14 @@ struct BotMessage: View {
241249
let lastMessage = chat.history.last
242250
return lastMessage?.role == .assistant && lastMessage?.id == id
243251
}
252+
253+
private func isThinkingStreaming() -> Bool {
254+
guard isLatestAssistantMessage(), chat.isReceivingMessage else { return false }
255+
switch message.turnStatus {
256+
case .success, .error, .cancelled: return false
257+
default: return true
258+
}
259+
}
244260
}
245261

246262
private struct TurnStatusView: View {

Core/Sources/ConversationTab/Views/ConversationAgentProgressView/ConversationAgentProgressView.swift

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,25 @@ import SwiftUI
1010
struct ProgressAgentRound: View {
1111
let rounds: [AgentRound]
1212
let chat: StoreOf<Chat>
13+
var isStreaming: Bool = false
1314

1415
var body: some View {
1516
WithPerceptionTracking {
1617
VStack(alignment: .leading, spacing: 8) {
17-
ForEach(rounds, id: \.roundId) { round in
18+
ForEach(Array(rounds.enumerated()), id: \.element.roundId) { roundIndex, round in
19+
let isLastRound = roundIndex == rounds.count - 1
1820
VStack(alignment: .leading, spacing: 8) {
19-
ThemedMarkdownText(text: round.reply, chat: chat)
21+
ForEach(Array(round.thinking.enumerated()), id: \.offset) { entryIndex, entry in
22+
ThinkingView(
23+
thinking: entry,
24+
isStreaming: isStreaming
25+
&& isLastRound
26+
&& entryIndex == round.thinking.count - 1
27+
)
28+
}
29+
if !round.reply.isEmpty {
30+
ThemedMarkdownText(text: round.reply, chat: chat)
31+
}
2032
if let toolCalls = round.toolCalls, !toolCalls.isEmpty {
2133
ProgressToolCalls(tools: toolCalls, chat: chat)
2234
}
@@ -42,7 +54,12 @@ struct SubAgentRounds: View {
4254
VStack(alignment: .leading, spacing: 8) {
4355
ForEach(rounds, id: \.roundId) { round in
4456
VStack(alignment: .leading, spacing: 8) {
45-
ThemedMarkdownText(text: round.reply, chat: chat)
57+
ForEach(Array(round.thinking.enumerated()), id: \.offset) { _, entry in
58+
ThinkingView(thinking: entry, isStreaming: false)
59+
}
60+
if !round.reply.isEmpty {
61+
ThemedMarkdownText(text: round.reply, chat: chat)
62+
}
4663
if let toolCalls = round.toolCalls, !toolCalls.isEmpty {
4764
ProgressToolCalls(tools: toolCalls, chat: chat)
4865
}
@@ -384,23 +401,56 @@ struct GenericToolTitleView: View {
384401
struct ProgressAgentRound_Preview: PreviewProvider {
385402
static let agentRounds: [AgentRound] = [
386403
.init(roundId: 1, reply: "this is agent step", toolCalls: [
404+
// Completed read file
387405
.init(
388406
id: "toolcall_001",
389-
name: "Tool Call 1",
390-
progressMessage: "Read Tool Call 1",
391-
status: .completed,
392-
error: nil),
407+
name: ServerToolName.readFile.rawValue,
408+
progressMessage: "Read src/AppDelegate.swift",
409+
status: .completed),
410+
// Completed file search with results
393411
.init(
394412
id: "toolcall_002",
395-
name: "Tool Call 2",
396-
progressMessage: "Running Tool Call 2",
413+
name: ServerToolName.findFiles.rawValue,
414+
progressMessage: "Searched for files matching query: **/*.swift",
415+
status: .completed,
416+
resultDetails: [
417+
.fileLocation(.init(uri: "file:///src/App.swift", range: .init(start: .init(line: 0, character: 0), end: .init(line: 10, character: 0)))),
418+
.fileLocation(.init(uri: "file:///src/Model.swift", range: .init(start: .init(line: 0, character: 0), end: .init(line: 5, character: 0)))),
419+
.fileLocation(.init(uri: "file:///src/ViewModel.swift", range: .init(start: .init(line: 0, character: 0), end: .init(line: 8, character: 0)))),
420+
]),
421+
// Completed create file (expandable)
422+
.init(
423+
id: "toolcall_003",
424+
name: ToolName.createFile.rawValue,
425+
progressMessage: "Created src/NewFeature.swift",
426+
status: .completed,
427+
result: [.text("```swift\nstruct NewFeature {\n var name: String\n}\n```")]),
428+
// Completed replace string (expandable)
429+
.init(
430+
id: "toolcall_004",
431+
name: ServerToolName.replaceString.rawValue,
432+
progressMessage: "Edited src/Config.swift",
433+
status: .completed,
434+
result: [.text("```diff\n- let version = \"1.0\"\n+ let version = \"2.0\"\n```")]),
435+
// Running tool
436+
.init(
437+
id: "toolcall_005",
438+
name: ServerToolName.codebase.rawValue,
439+
progressMessage: "Searching codebase for references",
397440
status: .running),
441+
// Error tool
442+
.init(
443+
id: "toolcall_006",
444+
name: ServerToolName.readFile.rawValue,
445+
progressMessage: "Read missing_file.swift",
446+
status: .error,
447+
error: "File not found"),
398448
]),
399449
]
400450

401451
static var previews: some View {
402452
let chatTabInfo = ChatTabInfo(id: "id", workspacePath: "path", username: "name")
403453
ProgressAgentRound(rounds: agentRounds, chat: .init(initialState: .init(), reducer: { Chat(service: ChatService.service(for: chatTabInfo)) }))
404-
.frame(width: 300, height: 300)
454+
.frame(width: 400, height: 500)
405455
}
406456
}

0 commit comments

Comments
 (0)