Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Orbit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.5;
MARKETING_VERSION = 1.0.6;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex";
PRODUCT_NAME = Orbit;
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -454,7 +454,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.5;
MARKETING_VERSION = 1.0.6;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex";
PRODUCT_NAME = Orbit;
REGISTER_APP_GROUPS = YES;
Expand All @@ -476,7 +476,7 @@
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0.5;
MARKETING_VERSION = 1.0.6;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.tests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -497,7 +497,7 @@
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.2;
MARKETING_VERSION = 1.0.5;
MARKETING_VERSION = 1.0.6;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.tests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -516,7 +516,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.5;
MARKETING_VERSION = 1.0.6;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.uitests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -535,7 +535,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6D7X9GGZAW;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.5;
MARKETING_VERSION = 1.0.6;
PRODUCT_BUNDLE_IDENTIFIER = "com.orbit.codex.uitests";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand Down
85 changes: 66 additions & 19 deletions Orbit/CodexAppServerActionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1507,16 +1507,19 @@ final class CodexAppServerActionProvider: ActionProvider {
return
}

streamedCommentaryBuffer.append(deltaText)
streamedCommentaryBuffer = Self.mergedCommentaryBuffer(
existing: streamedCommentaryBuffer,
incomingDelta: deltaText
)

if let update = visibleCommentaryUpdate(from: streamedCommentaryBuffer),
if let update = Self.visibleCommentaryUpdate(from: streamedCommentaryBuffer),
update != lastEmittedLiveCommentary {
lastEmittedLiveCommentary = update
eventHandler?(.liveUpdate(update))
}

guard !hasEmittedEarlyCommentary,
let snippet = speakableCommentarySnippet(from: streamedCommentaryBuffer) else {
let snippet = Self.speakableCommentarySnippet(from: streamedCommentaryBuffer) else {
return
}

Expand All @@ -1525,14 +1528,14 @@ final class CodexAppServerActionProvider: ActionProvider {
}

private func emitCompletedCommentaryIfNeeded(text: String) {
if let update = visibleCommentaryUpdate(from: text),
if let update = Self.visibleCommentaryUpdate(from: text),
update != lastEmittedLiveCommentary {
lastEmittedLiveCommentary = update
eventHandler?(.liveUpdate(update))
}

guard !hasEmittedEarlyCommentary,
let snippet = speakableCommentarySnippet(from: text) else {
let snippet = Self.speakableCommentarySnippet(from: text) else {
return
}

Expand Down Expand Up @@ -1837,30 +1840,64 @@ final class CodexAppServerActionProvider: ActionProvider {
return nil
}

private func speakableCommentarySnippet(from text: String) -> String? {
static func mergedCommentaryBuffer(existing: String, incomingDelta: String) -> String {
guard !incomingDelta.isEmpty else { return existing }
guard !existing.isEmpty else { return incomingDelta }

if existing.hasSuffix(incomingDelta) {
return existing
}

if incomingDelta.hasPrefix(existing) {
return incomingDelta
}

let maximumOverlap = min(existing.count, incomingDelta.count)
if maximumOverlap > 0 {
for overlapCount in stride(from: maximumOverlap, through: 1, by: -1) {
let existingSuffix = String(existing.suffix(overlapCount))
let incomingPrefix = String(incomingDelta.prefix(overlapCount))
if existingSuffix == incomingPrefix {
return existing + incomingDelta.dropFirst(overlapCount)
}
}
}

return existing + incomingDelta
}

static func speakableCommentarySnippet(from text: String) -> String? {
let cleaned = text
.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)

guard cleaned.count >= 18 else { return nil }
guard cleaned.count >= 28 else { return nil }

let firstSentence = cleaned.split(whereSeparator: { ".!?".contains($0) }).first.map(String.init) ?? cleaned
let candidate = firstSentence.trimmingCharacters(in: .whitespacesAndNewlines)
guard candidate.split(separator: " ").count >= 3 else { return nil }

if candidate.count <= 72 {
return candidate.hasSuffix(".") ? candidate : "\(candidate)."
if let firstCompletedSentence = firstCompletedSentence(in: cleaned) {
let candidate = firstCompletedSentence.trimmingCharacters(in: .whitespacesAndNewlines)
guard candidate.split(separator: " ").count >= 4 else { return nil }
return candidate
}

let prefix = String(candidate.prefix(69))
let trimmed = prefix
.replacingOccurrences(of: "\\s+\\S*$", with: "", options: .regularExpression)
guard cleaned.count >= 42, cleaned.split(separator: " ").count >= 7 else { return nil }

let maximumLength = 120
let wasTruncated = cleaned.count > maximumLength
let prefix = String(cleaned.prefix(maximumLength))
let trimmed = (wasTruncated
? prefix.replacingOccurrences(of: "\\s+\\S*$", with: "", options: .regularExpression)
: prefix)
.replacingOccurrences(
of: "\\b(?:and|or|to|for|with|of|in|on|at|by|from|about|into|over|after|before|without|using)$",
with: "",
options: .regularExpression
)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
return "\(trimmed)..."
guard trimmed.split(separator: " ").count >= 4 else { return nil }
return trimmed.hasSuffix(".") ? trimmed : "\(trimmed)."
}

private func visibleCommentaryUpdate(from text: String) -> String? {
static func visibleCommentaryUpdate(from text: String) -> String? {
let cleaned = text
.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
Expand All @@ -1884,6 +1921,16 @@ final class CodexAppServerActionProvider: ActionProvider {
return trimmed.isEmpty ? nil : "\(trimmed)..."
}

private static func firstCompletedSentence(in text: String) -> String? {
guard let terminatorIndex = text.firstIndex(where: { ".!?".contains($0) }) else {
return nil
}

let sentence = String(text[...terminatorIndex])
.trimmingCharacters(in: .whitespacesAndNewlines)
return sentence.isEmpty ? nil : sentence
}

private func updateModelCatalog(from result: [String: Any]) {
let parsedModels = Self.parseModelCatalog(from: result)
guard !parsedModels.isEmpty else { return }
Expand Down
19 changes: 16 additions & 3 deletions Orbit/OrbitManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,6 @@ final class OrbitManager: ObservableObject {
handleEarlyActionCommentary(commentary)
case .liveUpdate(let update):
activeActionDetailLine = update
appendActionUpdate(update)
showCodexActivityOverlayCard()
case .toolPrompt(let prompt):
pendingToolPrompt = prompt
Expand Down Expand Up @@ -765,8 +764,6 @@ final class OrbitManager: ObservableObject {
scheduleTransientHideIfNeeded()
case .completed(let summary):
cancelActionAcknowledgementFlow()
textToSpeechProvider.stopPlayback()
fallbackTextToSpeechProvider.stopPlayback()
pendingToolPrompt = nil
handleCompletedCodexSummary(summary)
case .failed(let errorMessage):
Expand Down Expand Up @@ -1154,6 +1151,7 @@ final class OrbitManager: ObservableObject {
let trimmedFallback = fallback?.trimmingCharacters(in: .whitespacesAndNewlines)
let finalText = (trimmedPrimary?.isEmpty == false ? trimmedPrimary : trimmedFallback) ?? "done."

await waitForCurrentSpeechToSettle(maximumWait: 2.4)
voiceState = .responding

do {
Expand All @@ -1167,6 +1165,21 @@ final class OrbitManager: ObservableObject {
scheduleTransientHideIfNeeded()
}

private func waitForCurrentSpeechToSettle(maximumWait: TimeInterval) async {
let deadline = Date().addingTimeInterval(maximumWait)
while (textToSpeechProvider.isPlaying || fallbackTextToSpeechProvider.isPlaying),
Date() < deadline {
try? await Task.sleep(nanoseconds: 120_000_000)
}

if textToSpeechProvider.isPlaying {
textToSpeechProvider.stopPlayback()
}
if fallbackTextToSpeechProvider.isPlaying {
fallbackTextToSpeechProvider.stopPlayback()
}
}

private func scheduleActionAcknowledgementFallback() {
actionAcknowledgementTask?.cancel()
actionAcknowledgementTask = nil
Expand Down
16 changes: 14 additions & 2 deletions Orbit/OrbitPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -550,9 +550,9 @@ struct OrbitPanelView: View {
}
}
.padding(.top, 2)
} else if !orbitManager.recentActionUpdates.isEmpty {
} else if !visibleRecentActionUpdates.isEmpty {
VStack(alignment: .leading, spacing: 5) {
ForEach(Array(orbitManager.recentActionUpdates.suffix(4).enumerated()), id: \.offset) { _, update in
ForEach(Array(visibleRecentActionUpdates.enumerated()), id: \.offset) { _, update in
HStack(alignment: .top, spacing: 6) {
Circle()
.fill(Color.white.opacity(0.55))
Expand Down Expand Up @@ -1330,6 +1330,18 @@ struct OrbitPanelView: View {
}
}

private var visibleRecentActionUpdates: [String] {
let activeDetail = orbitManager.activeActionDetailLine?
.trimmingCharacters(in: .whitespacesAndNewlines)

let filtered = orbitManager.recentActionUpdates.filter { update in
guard let activeDetail, !activeDetail.isEmpty else { return true }
return update.trimmingCharacters(in: .whitespacesAndNewlines) != activeDetail
}

return Array(filtered.suffix(4))
}

private var codexDetailLine: String? {
if let detail = orbitManager.activeActionDetailLine {
return detail
Expand Down
25 changes: 25 additions & 0 deletions OrbitTests/OrbitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,29 @@ struct OrbitTests {
#expect(parsed.first?.supportedEfforts == [.medium, .high])
#expect(parsed.first?.defaultEffort == .medium)
}

@Test func commentaryBufferMergeAvoidsOverlappingStreamDuplication() async throws {
let merged = CodexAppServerActionProvider.mergedCommentaryBuffer(
existing: "Using the pdf skill to make a polished",
incomingDelta: " polished illustrated PDF"
)

#expect(merged == "Using the pdf skill to make a polished illustrated PDF")
}

@Test func earlyCommentarySpeechWaitsForStableChunk() async throws {
let tooEarly = CodexAppServerActionProvider.speakableCommentarySnippet(
from: "Using the pdf skill"
)
let stableSnippet = CodexAppServerActionProvider.speakableCommentarySnippet(
from: "Using the pdf skill to make a polished illustrated PDF and save it for you"
)
let completedSentence = CodexAppServerActionProvider.speakableCommentarySnippet(
from: "Opening the browser now. Then I'll sign you in."
)

#expect(tooEarly == nil)
#expect(stableSnippet == "Using the pdf skill to make a polished illustrated PDF and save it for you.")
#expect(completedSentence == "Opening the browser now.")
}
}
Loading