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
2 changes: 1 addition & 1 deletion desktop/Desktop/Sources/FloatingControlBar/AgentPill.swift
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ final class AgentPillsManager: ObservableObject {
pill.query,
model: pill.model,
systemPromptSuffix: systemPromptSuffix,
systemPromptPrefix: ChatProvider.floatingBarSystemPromptPrefix,
systemPromptStyle: .floating,
sessionKey: "agent-\(pill.id.uuidString)"
)
self.complete(pill: pill, provider: provider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1707,7 +1707,7 @@ class FloatingControlBarManager {
message,
model: floatingModel,
systemPromptSuffix: notificationContextSuffix,
systemPromptPrefix: ChatProvider.floatingBarSystemPromptPrefix,
systemPromptStyle: .floating,
sessionKey: floatingSessionKey,
imageData: screenshotData
)
Expand Down
46 changes: 36 additions & 10 deletions desktop/Desktop/Sources/Providers/ChatProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,14 @@ enum ChatMode: String, CaseIterable {
case act
}

enum ChatSystemPromptStyle {
case main
case floating

var includesDatabaseSchema: Bool { self == .main }
var includesSkills: Bool { self == .main }
}

/// State management for chat functionality with Claude Agent SDK
/// Uses hybrid architecture: Swift → Claude Agent (via Node.js bridge) for AI, Backend for persistence + context
@MainActor
Expand Down Expand Up @@ -651,6 +659,7 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
/// Conversation history from before app launch IS included (via buildConversationHistory());
/// after session/new the ACP SDK tracks ongoing history natively.
private var cachedMainSystemPrompt: String = ""
private var cachedFloatingSystemPrompt: String = ""

// MARK: - CLAUDE.md & Skills (Global)
@Published var claudeMdContent: String?
Expand Down Expand Up @@ -914,12 +923,14 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
)
// Pre-warm ACP sessions with their respective system prompts.
// This is the only place the system prompt is built and applied.
let mainSystemPrompt = buildSystemPrompt(contextString: formatMemoriesSection())
let floatingSystemPrompt = Self.floatingBarSystemPromptPrefix + "\n\n" + mainSystemPrompt
let promptContext = formatMemoriesSection()
let mainSystemPrompt = buildSystemPrompt(contextString: promptContext, style: .main)
let floatingSystemPrompt = buildFloatingBarSystemPrompt(contextString: promptContext)
let floatingModel = ShortcutSettings.shared.selectedModel.isEmpty
? ModelQoS.Claude.defaultSelection
: ShortcutSettings.shared.selectedModel
cachedMainSystemPrompt = mainSystemPrompt
cachedFloatingSystemPrompt = floatingSystemPrompt
await agentBridge.warmupSession(cwd: workingDirectory, sessions: [
.init(key: "main", model: ModelQoS.Claude.chat, systemPrompt: mainSystemPrompt),
.init(key: "floating", model: floatingModel, systemPrompt: floatingSystemPrompt)
Expand Down Expand Up @@ -1618,7 +1629,7 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
/// Called once at warmup (via ensureBridgeStarted) and cached in cachedMainSystemPrompt.
/// Conversation history is injected here so the brand-new ACP session starts with context
/// from before the app launch. After session/new the ACP SDK owns history natively.
private func buildSystemPrompt(contextString: String) -> String {
private func buildSystemPrompt(contextString: String, style: ChatSystemPromptStyle) -> String {
// Get user name from AuthService
let userName = AuthService.shared.displayName.isEmpty ? "there" : AuthService.shared.givenName

Expand All @@ -1638,7 +1649,7 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
goalSection: goalSection,
tasksSection: tasksSection,
aiProfileSection: aiProfileSection,
databaseSchema: cachedDatabaseSchema
databaseSchema: style.includesDatabaseSchema ? cachedDatabaseSchema : ""
)

// Inject conversation history so the new ACP session has context from before app launch.
Expand All @@ -1662,7 +1673,7 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
// Append enabled skills as available context (global + project)
// dev-mode is included in the list when devModeEnabled; full content loaded on demand via load_skill
let enabledSkillNames = getEnabledSkillNames()
if !enabledSkillNames.isEmpty {
if style.includesSkills && !enabledSkillNames.isEmpty {
let allSkills = discoveredSkills + projectDiscoveredSkills
let skillNames = allSkills
.filter { enabledSkillNames.contains($0.name) && ($0.name != "dev-mode" || devModeEnabled) }
Expand All @@ -1678,7 +1689,7 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
let historyInjected = !history.isEmpty
let historyMessages = messages.filter { !$0.text.isEmpty && !$0.isStreaming }
let historyCount = min(historyMessages.count, 20)
log("ChatProvider: prompt built — schema: \(!cachedDatabaseSchema.isEmpty ? "yes" : "no"), goals: \(activeGoalCount), tasks: \(cachedTasks.count), ai_profile: \(!cachedAIProfile.isEmpty ? "yes" : "no"), memories: \(cachedMemories.count), history: \(historyInjected ? "injected (\(historyCount) msgs)" : "none"), claude_md: \(claudeMdEnabled && claudeMdContent != nil ? "yes" : "no"), project_claude_md: \(projectClaudeMdEnabled && projectClaudeMdContent != nil ? "yes" : "no"), skills: \(enabledSkillNames.count), dev_mode_in_skills: \(devModeEnabled && devModeContext != nil ? "yes" : "no"), prompt_length: \(prompt.count) chars")
log("ChatProvider: prompt built — schema: \(style.includesDatabaseSchema && !cachedDatabaseSchema.isEmpty ? "yes" : "no"), goals: \(activeGoalCount), tasks: \(cachedTasks.count), ai_profile: \(!cachedAIProfile.isEmpty ? "yes" : "no"), memories: \(cachedMemories.count), history: \(historyInjected ? "injected (\(historyCount) msgs)" : "none"), claude_md: \(claudeMdEnabled && claudeMdContent != nil ? "yes" : "no"), project_claude_md: \(projectClaudeMdEnabled && projectClaudeMdContent != nil ? "yes" : "no"), skills: \(style.includesSkills ? enabledSkillNames.count : 0), dev_mode_in_skills: \(style.includesSkills && devModeEnabled && devModeContext != nil ? "yes" : "no"), prompt_length: \(prompt.count) chars")

// Log per-section character breakdown
let baseTemplate = ChatPromptBuilder.buildDesktopChat(
Expand All @@ -1693,15 +1704,23 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
"goals:\(goalSection.count)c, " +
"tasks:\(tasksSection.count)c, " +
"ai_profile:\(aiProfileSection.count)c, " +
"schema:\(cachedDatabaseSchema.count)c, " +
"schema:\(style.includesDatabaseSchema ? cachedDatabaseSchema.count : 0)c, " +
"history:\(history.count)c, " +
"claude_md:\(claudeMdContent?.count ?? 0)c, " +
"project_claude_md:\(projectClaudeMdContent?.count ?? 0)c, " +
"skills:\(skillsSectionSize)c")
"skills:\(style.includesSkills ? skillsSectionSize : 0)c")

return prompt
}

private func buildFloatingBarSystemPrompt(contextString: String) -> String {
let prompt = buildSystemPrompt(
contextString: contextString,
style: .floating
)
return Self.floatingBarSystemPromptPrefix + "\n\n" + prompt
}

/// Build system prompt for task chat sessions.
func buildTaskChatSystemPrompt() -> String {
let userName = AuthService.shared.displayName.isEmpty ? "there" : AuthService.shared.givenName
Expand Down Expand Up @@ -2432,7 +2451,7 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
/// - Parameters:
/// - text: The message text
/// - model: Optional model override for this query (e.g. "claude-sonnet-4-6" for floating bar)
func sendMessage(_ text: String, model: String? = nil, isFollowUp: Bool = false, systemPromptSuffix: String? = nil, systemPromptPrefix: String? = nil, sessionKey: String? = nil, resume: String? = nil, imageData: Data? = nil) async {
func sendMessage(_ text: String, model: String? = nil, isFollowUp: Bool = false, systemPromptSuffix: String? = nil, systemPromptPrefix: String? = nil, systemPromptStyle: ChatSystemPromptStyle = .main, sessionKey: String? = nil, resume: String? = nil, imageData: Data? = nil) async {
let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedText.isEmpty else { return }

Expand Down Expand Up @@ -2616,7 +2635,14 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
// with the onboarding deep-dive step.
systemPrompt = prefix
} else {
systemPrompt = cachedMainSystemPrompt
if systemPromptStyle == .floating {
if cachedFloatingSystemPrompt.isEmpty {
cachedFloatingSystemPrompt = buildFloatingBarSystemPrompt(contextString: formatMemoriesSection())
}
systemPrompt = cachedFloatingSystemPrompt
} else {
Comment on lines +2638 to +2643
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The fallback branch computes a fresh floating prompt but never writes it back to cachedFloatingSystemPrompt. If warmup failed (bridge start returned early before setting the cache), every subsequent floating query triggers a full buildFloatingBarSystemPrompt rebuild instead of amortising that cost after the first call. Caching the result here mirrors exactly what warmup does and avoids repeated rebuilds.

Suggested change
if systemPromptStyle == .floating {
systemPrompt = cachedFloatingSystemPrompt.isEmpty
? buildFloatingBarSystemPrompt(contextString: formatMemoriesSection())
: cachedFloatingSystemPrompt
} else {
if systemPromptStyle == .floating {
if cachedFloatingSystemPrompt.isEmpty {
cachedFloatingSystemPrompt = buildFloatingBarSystemPrompt(contextString: formatMemoriesSection())
}
systemPrompt = cachedFloatingSystemPrompt
} else {

systemPrompt = cachedMainSystemPrompt
}
if let prefix = systemPromptPrefix, !prefix.isEmpty {
systemPrompt = prefix + "\n\n" + systemPrompt
}
Expand Down
Loading