diff --git a/.gitignore b/.gitignore index 29f4778..b86adfe 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,5 @@ iOSInjectionProject/ Network Trash Folder Temporary Items .apdisk + +CLAUDE.md \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..977bb97 --- /dev/null +++ b/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "oAI", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "oAI", + targets: ["oAI"] + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "oAI" + ), + .testTarget( + name: "oAITests", + dependencies: ["oAI"] + ), + ] +) diff --git a/Sources/oAI/oAI.swift b/Sources/oAI/oAI.swift new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/Sources/oAI/oAI.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/Tests/oAITests/oAITests.swift b/Tests/oAITests/oAITests.swift new file mode 100644 index 0000000..f3c6191 --- /dev/null +++ b/Tests/oAITests/oAITests.swift @@ -0,0 +1,6 @@ +import Testing +@testable import oAI + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/oAI.xcodeproj/project.pbxproj b/oAI.xcodeproj/project.pbxproj index fff3c41..e95dcf3 100644 --- a/oAI.xcodeproj/project.pbxproj +++ b/oAI.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A52B47522F3E45BA004200E2 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A52B47512F3E45BA004200E2 /* MarkdownUI */; }; A550A8342F3C5C9300136F2B /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = A550A6812F3B730000136F2B /* GRDB */; }; /* End PBXBuildFile section */ @@ -28,6 +29,7 @@ buildActionMask = 2147483647; files = ( A550A8342F3C5C9300136F2B /* GRDB in Frameworks */, + A52B47522F3E45BA004200E2 /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -71,6 +73,7 @@ name = oAI; packageProductDependencies = ( A550A6812F3B730000136F2B /* GRDB */, + A52B47512F3E45BA004200E2 /* MarkdownUI */, ); productName = oAI; productReference = A550A6622F3B72EA00136F2B /* oAI.app */; @@ -102,6 +105,7 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( A550A6802F3B730000136F2B /* XCRemoteSwiftPackageReference "GRDB" */, + A52B47502F3E45BA004200E2 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, ); preferredProjectObjectVersion = 77; productRefGroup = A550A6632F3B72EA00136F2B /* Products */; @@ -358,6 +362,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + A52B47502F3E45BA004200E2 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.4.1; + }; + }; A550A6802F3B730000136F2B /* XCRemoteSwiftPackageReference "GRDB" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/groue/GRDB.swift"; @@ -369,6 +381,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + A52B47512F3E45BA004200E2 /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = A52B47502F3E45BA004200E2 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; A550A6812F3B730000136F2B /* GRDB */ = { isa = XCSwiftPackageProductDependency; package = A550A6802F3B730000136F2B /* XCRemoteSwiftPackageReference "GRDB" */; diff --git a/oAI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/oAI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 17fbdb6..6c96f64 100644 --- a/oAI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/oAI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d77223ea3cadaebd2154378ec5005b6ebefcef3b34a4dafa368b0c4f16c0561c", + "originHash" : "3d7837e8d2f446123857e5e76a8365b4c8fbf995fe10eb02a8b5592491655b0b", "pins" : [ { "identity" : "grdb.swift", @@ -9,6 +9,33 @@ "revision" : "aa0079aeb82a4bf00324561a40bffe68c6fe1c26", "version" : "7.9.0" } + }, + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "2849f5323265386e200484b0d0f896e73c3411b9", + "version" : "6.0.1" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark", + "state" : { + "revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe", + "version" : "0.7.1" + } + }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui", + "state" : { + "revision" : "5f613358148239d0292c0cef674a3c2314737f9e", + "version" : "2.4.1" + } } ], "version" : 3 diff --git a/oAI/Resources/oAI.help/Contents/Resources/en.lproj/index.html b/oAI/Resources/oAI.help/Contents/Resources/en.lproj/index.html index 7f7c32f..4510c8a 100644 --- a/oAI/Resources/oAI.help/Contents/Resources/en.lproj/index.html +++ b/oAI/Resources/oAI.help/Contents/Resources/en.lproj/index.html @@ -543,7 +543,7 @@ diff --git a/oAI/ViewModels/ChatViewModel.swift b/oAI/ViewModels/ChatViewModel.swift index 32b1546..da19a95 100644 --- a/oAI/ViewModels/ChatViewModel.swift +++ b/oAI/ViewModels/ChatViewModel.swift @@ -57,6 +57,21 @@ You are a helpful AI assistant. Follow these guidelines: 5. **Be Direct**: Provide concise, relevant answers. Avoid unnecessary preambles or apologies. +6. **Use Markdown Formatting**: Always format your responses using standard Markdown syntax: + - Use **bold** for emphasis + - Use bullet points and numbered lists for organization + - Use code blocks with language tags for code (e.g., ```python) + - Use proper headings (##, ###) to structure long responses + - If the user requests output in other formats (HTML, JSON, XML, etc.), wrap those in appropriate code blocks + +7. **Break Down Complex Tasks**: When working with tools (file access, search, etc.), break complex tasks into smaller, manageable steps. If a task requires many operations: + - Complete one logical step at a time + - Present findings or progress after each step + - Ask the user if you should continue to the next step + - Be mindful of tool usage limits (typically 25-30 tool calls per request) + +8. **Incremental Progress**: For large codebases or complex analyses, work incrementally. Don't try to explore everything at once. Focus on what's immediately relevant to the user's question. + Remember: It's better to ask questions or admit uncertainty than to provide incorrect or fabricated information. """ diff --git a/oAI/Views/Main/InputBar.swift b/oAI/Views/Main/InputBar.swift index 3c4715d..5d63757 100644 --- a/oAI/Views/Main/InputBar.swift +++ b/oAI/Views/Main/InputBar.swift @@ -133,7 +133,12 @@ struct InputBar: View { } return .ignored } - .onKeyPress(.return) { + .onKeyPress(.return, phases: .down) { press in + // Shift+Return: always insert newline (let system handle) + if press.modifiers.contains(.shift) { + return .ignored + } + // If command dropdown is showing, select the highlighted command if showCommandDropdown { let suggestions = CommandSuggestionsView.filteredCommands(for: text) diff --git a/oAI/Views/Main/MarkdownContentView.swift b/oAI/Views/Main/MarkdownContentView.swift index e13c5eb..3c74d38 100644 --- a/oAI/Views/Main/MarkdownContentView.swift +++ b/oAI/Views/Main/MarkdownContentView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import MarkdownUI #if canImport(AppKit) import AppKit #endif @@ -34,21 +35,16 @@ struct MarkdownContentView: View { @ViewBuilder private func markdownText(_ text: String) -> some View { - if let attrString = try? AttributedString(markdown: text, options: .init(interpretedSyntax: .full)) { - Text(attrString) - .font(.system(size: fontSize)) - .foregroundColor(.oaiPrimary) - .lineSpacing(4) - .frame(maxWidth: .infinity, alignment: .leading) - .textSelection(.enabled) - } else { - Text(text) - .font(.system(size: fontSize)) - .foregroundColor(.oaiPrimary) - .lineSpacing(4) - .frame(maxWidth: .infinity, alignment: .leading) - .textSelection(.enabled) - } + Markdown(text) + .markdownTextStyle { + FontSize(fontSize) + ForegroundColor(.primary) + } + .markdownBlockStyle(\.paragraph) { configuration in + configuration.label + .markdownMargin(top: 0, bottom: 8) + } + .textSelection(.enabled) } // MARK: - Parsing diff --git a/oAI/Views/Main/MessageRow.swift b/oAI/Views/Main/MessageRow.swift index c22397d..617ec75 100644 --- a/oAI/Views/Main/MessageRow.swift +++ b/oAI/Views/Main/MessageRow.swift @@ -20,6 +20,16 @@ struct MessageRow: View { #endif var body: some View { + // Compact layout for system messages (tool calls) + if message.role == .system && !isErrorMessage { + compactSystemMessage + } else { + standardMessageLayout + } + } + + @ViewBuilder + private var standardMessageLayout: some View { HStack(alignment: .top, spacing: 12) { // Role icon roleIcon @@ -131,6 +141,34 @@ struct MessageRow: View { #endif } + // Close standardMessageLayout - the above closing braces close it + // The body: some View now handles the split between compact and standard + + // MARK: - Compact System Message + + @ViewBuilder + private var compactSystemMessage: some View { + HStack(spacing: 8) { + Image(systemName: "wrench.and.screwdriver") + .font(.system(size: 11)) + .foregroundColor(.secondary) + + Text(message.content) + .font(.system(size: 11)) + .foregroundColor(.secondary) + + Spacer() + + Text(message.timestamp, style: .time) + .font(.system(size: 10)) + .foregroundColor(.secondary.opacity(0.7)) + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.secondary.opacity(0.08)) + .cornerRadius(6) + } + // MARK: - Message Content @ViewBuilder @@ -158,7 +196,13 @@ struct MessageRow: View { .textSelection(.enabled) } case .user: - MarkdownContentView(content: message.content, fontSize: settings.dialogTextSize) + // User messages: preserve line breaks as-is (plain text, not markdown) + Text(message.content) + .font(.system(size: settings.dialogTextSize)) + .foregroundColor(.oaiPrimary) + .lineSpacing(4) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) } } diff --git a/oAI/Views/Screens/AboutView.swift b/oAI/Views/Screens/AboutView.swift index 29f6353..a8b7773 100644 --- a/oAI/Views/Screens/AboutView.swift +++ b/oAI/Views/Screens/AboutView.swift @@ -44,7 +44,7 @@ struct AboutView: View { .padding(.horizontal, 40) VStack(spacing: 4) { - Text("© 2026 [Rune Olsen](https://blog.rune.pm)") + Text("© 2026 [Rune Olsen](https://blog.rune.pm) | [Support](mailto:support@fubar.pm)") .font(.caption) .foregroundStyle(.secondary) diff --git a/oAI/Views/Screens/SettingsView.swift b/oAI/Views/Screens/SettingsView.swift index 0a91350..a29072b 100644 --- a/oAI/Views/Screens/SettingsView.swift +++ b/oAI/Views/Screens/SettingsView.swift @@ -43,6 +43,21 @@ You are a helpful AI assistant. Follow these guidelines: 5. **Be Direct**: Provide concise, relevant answers. Avoid unnecessary preambles or apologies. +6. **Use Markdown Formatting**: Always format your responses using standard Markdown syntax: + - Use **bold** for emphasis + - Use bullet points and numbered lists for organization + - Use code blocks with language tags for code (e.g., ```python) + - Use proper headings (##, ###) to structure long responses + - If the user requests output in other formats (HTML, JSON, XML, etc.), wrap those in appropriate code blocks + +7. **Break Down Complex Tasks**: When working with tools (file access, search, etc.), break complex tasks into smaller, manageable steps. If a task requires many operations: + - Complete one logical step at a time + - Present findings or progress after each step + - Ask the user if you should continue to the next step + - Be mindful of tool usage limits (typically 25-30 tool calls per request) + +8. **Incremental Progress**: For large codebases or complex analyses, work incrementally. Don't try to explore everything at once. Focus on what's immediately relevant to the user's question. + Remember: It's better to ask questions or admit uncertainty than to provide incorrect or fabricated information. """