native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(repo-appearance): accept any image format the system can render

Previously the importer enforced a PNG/SVG whitelist and threw
`RepositoryIconAssetStoreError.unsupportedExtension` for everything
else. The whitelist was redundant — the file picker already filters
to image UTTypes, and a corrupt or unrenderable file falls back to
the dashed-questionmark placeholder via `RepositoryIconImage`. Drop
the gate so JPG / WebP / HEIC / GIF / TIFF / BMP / etc. all work
without any extra plumbing.

- Remove `RepositoryIconAssetStoreError` and the extension whitelist
in the live store. Files with no extension fall back to a generic
`.img` suffix on the destination filename so the round-trip still
works.
- Reducer's import effect collapses to a single `catch error in`
branch using `error.localizedDescription`; the dedicated
`errorMessage(for:)` helper is gone.
- Picker's `fileImporter` widens from `[.png, .svg]` to
`[.image, .svg]`. SVG stays explicit because UTType conformance
for SVG-as-`.image` has been spotty across older OS revisions —
redundant on macOS 26+ but cheap insurance.
- `isTintable` logic unchanged — only `.svg` filenames get the
template-tint branch; bitmaps render with their own colors. Help
text generalized: "PNG icons" → "Bitmap icons", picker subtitle
same.
- Tests: drop `importImageRejectsUnsupportedExtension` (no longer
applicable) and `importFailureSurfacesErrorMessage` (covered by
the generic error path test). Add coverage for jpg/jpeg/webp/heic/
gif/tiff/bmp acceptance and for files with no extension.

onevcat c2e0d7cc 66366f11

+46 -47
+11 -13
supacode/Clients/Repositories/RepositoryIconAssetStore.swift
··· 39 39 ) -> Bool 40 40 } 41 41 42 - nonisolated enum RepositoryIconAssetStoreError: Error, Equatable { 43 - case unsupportedExtension(String) 44 - } 45 - 46 42 nonisolated extension RepositoryIconAssetStore { 47 - /// Allowed input extensions. PNG and SVG only — JPEG and other 48 - /// formats either don't suit repo icon use (no transparency) or 49 - /// don't render well at small sidebar sizes. 50 - static let supportedExtensions: Set<String> = ["png", "svg"] 51 - 52 43 static var liveValue: RepositoryIconAssetStore { 53 44 RepositoryIconAssetStore( 54 45 importImage: { sourceURL, rootURL in 55 - let normalizedExt = sourceURL.pathExtension.lowercased() 56 - guard Self.supportedExtensions.contains(normalizedExt) else { 57 - throw RepositoryIconAssetStoreError.unsupportedExtension(normalizedExt) 58 - } 46 + // No extension whitelist — the file picker filters down to 47 + // image UTTypes already, and anything that NSImage can't 48 + // render later falls back to the dashed-questionmark 49 + // placeholder in `RepositoryIconImage`. The `.svg` suffix 50 + // remains the lone meaningful signal because it gates the 51 + // template-tinting branch downstream; everything else is 52 + // treated as an opaque bitmap. 53 + let normalizedExt = 54 + sourceURL.pathExtension.lowercased().isEmpty 55 + ? "img" 56 + : sourceURL.pathExtension.lowercased() 59 57 let directory = SupacodePaths.repositoryIconsDirectory(for: rootURL) 60 58 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) 61 59 let filename = "\(UUID().uuidString.lowercased()).\(normalizedExt)"
-8
supacode/Features/RepositorySettings/Reducer/RepositorySettingsFeature.swift
··· 222 222 do { 223 223 let filename = try store.importImage(sourceURL, rootURL) 224 224 await send(.userImageImported(filename: filename)) 225 - } catch let error as RepositoryIconAssetStoreError { 226 - await send(.userImageImportFailed(Self.errorMessage(for: error))) 227 225 } catch { 228 226 await send(.userImageImportFailed(error.localizedDescription)) 229 227 } ··· 326 324 } 327 325 } 328 326 329 - private static func errorMessage(for error: RepositoryIconAssetStoreError) -> String { 330 - switch error { 331 - case .unsupportedExtension(let ext): 332 - return "Repository icons must be PNG or SVG. \(ext.uppercased()) files aren't supported." 333 - } 334 - } 335 327 }
+9 -3
supacode/Features/RepositorySettings/Views/RepositoryAppearancePickerView.swift
··· 39 39 title: "Repository Icon", 40 40 subtitle: 41 41 "Pick a preset or enter any SF Symbol name. SVG and SF Symbol icons are tinted " 42 - + "with the repo color; PNG keeps its own colors.", 42 + + "with the repo color; bitmap formats keep their own colors.", 43 43 presets: RepositoryIconPresets.presets, 44 44 onApply: { applySymbolFromPicker($0) }, 45 45 onCancel: { isSymbolPickerPresented = false } 46 46 ) 47 47 } 48 + // Accept any image UTType — PNG / JPEG / WebP / HEIC / TIFF / GIF 49 + // / etc. all flow through the same `NSImage(contentsOf:)` render 50 + // path. SVG is listed explicitly because it's a structured-text 51 + // format that doesn't always conform to `.image` in older 52 + // UTType conformance tables. Anything that fails to decode falls 53 + // back to the dashed placeholder at render time. 48 54 .fileImporter( 49 55 isPresented: $isImageImporterPresented, 50 - allowedContentTypes: [.png, .svg], 56 + allowedContentTypes: [.image, .svg], 51 57 allowsMultipleSelection: false 52 58 ) { result in 53 59 handleImageImportResult(result) ··· 171 177 private var iconHelpText: String { 172 178 switch store.appearance.icon { 173 179 case .userImage(let filename) where !filename.lowercased().hasSuffix(".svg"): 174 - return "PNG icons keep their original colors and ignore the repo color." 180 + return "Bitmap icons keep their original colors and ignore the repo color." 175 181 case .userImage: 176 182 return "User-provided SVGs are tinted with the repo color." 177 183 case .sfSymbol:
+26 -5
supacodeTests/RepositoryIconAssetStoreTests.swift
··· 80 80 #expect(filename.hasSuffix(".svg")) 81 81 } 82 82 83 - @Test func importImageRejectsUnsupportedExtension() throws { 83 + @Test func importImageAcceptsArbitraryImageExtensions() throws { 84 + // The store no longer enforces a PNG/SVG whitelist — the file 85 + // picker filters down to image UTTypes already, and anything 86 + // that NSImage can't decode falls back to a placeholder at 87 + // render time. JPG / WebP / HEIC / GIF / TIFF / etc. all flow 88 + // through the same byte-copy path and round-trip through 89 + // `repositoryIconFileURL` like PNG does. 90 + let store = RepositoryIconAssetStore.liveValue 91 + let repoRoot = makeRepoRootScratch() 92 + 93 + for ext in ["jpg", "jpeg", "webp", "heic", "gif", "tiff", "bmp"] { 94 + let source = ScratchDirectory(prefix: "prowl-icon-source") 95 + let sourceFile = try writeSourceFile(in: source, extension: ext) 96 + let filename = try store.importImage(sourceFile, repoRoot.url) 97 + #expect(filename.hasSuffix(".\(ext)")) 98 + } 99 + } 100 + 101 + @Test func importImageHandlesFileWithNoExtension() throws { 102 + // Defensive: a dragged-in file without an extension shouldn't 103 + // crash the importer. The destination filename gets a generic 104 + // fallback so the round-trip still works. 84 105 let store = RepositoryIconAssetStore.liveValue 85 106 let repoRoot = makeRepoRootScratch() 86 107 let source = ScratchDirectory(prefix: "prowl-icon-source") 87 - let sourceFile = try writeSourceFile(in: source, extension: "jpeg") 108 + let sourceFile = source.url.appending(path: "icon", directoryHint: .notDirectory) 109 + try Data([0xDE, 0xAD]).write(to: sourceFile) 88 110 89 - #expect(throws: RepositoryIconAssetStoreError.self) { 90 - _ = try store.importImage(sourceFile, repoRoot.url) 91 - } 111 + let filename = try store.importImage(sourceFile, repoRoot.url) 112 + #expect(!filename.isEmpty) 92 113 } 93 114 94 115 @Test func importImageCreatesIconsDirectoryWhenMissing() throws {
-18
supacodeTests/RepositorySettingsAppearanceTests.swift
··· 179 179 await store.finish() 180 180 } 181 181 182 - @Test func importFailureSurfacesErrorMessage() async throws { 183 - let store = makeStore( 184 - iconAssetStore: RepositoryIconAssetStore( 185 - importImage: { _, _ in throw RepositoryIconAssetStoreError.unsupportedExtension("jpeg") }, 186 - remove: { _, _ in }, 187 - exists: { _, _ in false } 188 - ) 189 - ) 190 - 191 - await store.send(.importUserImage(URL(fileURLWithPath: "/tmp/source.jpeg"))) 192 - await store.receive(\.userImageImportFailed) { 193 - $0.appearanceImportError = """ 194 - Repository icons must be PNG or SVG. JPEG files aren't supported. 195 - """.trimmingCharacters(in: .whitespacesAndNewlines) 196 - } 197 - await store.finish() 198 - } 199 - 200 182 @Test func importGenericErrorSurfacesLocalizedDescription() async throws { 201 183 struct Boom: LocalizedError { var errorDescription: String? { "boom" } } 202 184 let store = makeStore(