native macOS codings agent orchestrator
6
fork

Configure Feed

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

test(terminal): pure-function tests for icon detector + map + source attribution

Adds 32 tests covering the side-effect-free layers of the auto-tab-
icon stack so further mapping additions / heuristic tweaks can't
silently regress:

- `CommandIconMapTests`: exact / case-insensitive lookup, first-token
extraction with args, alias coverage (npx→Npm, lazygit→Git, …),
unknown returns nil, debug-catalog ordering and must-have tokens.
- `TabIconSourceTests`: `storageString` encoding for SF-Symbol-only
vs asset-bearing entries, `ResolvedTabIcon.parse` decoding
(including asset names with spaces and defensive colon handling),
full round-trip in both directions.
- `IconDetectorIdleHeuristicTests`: covers `user@host:path`,
`user@host:/path`, `~/path`, `/abs/path`, and `…/compact/path`
prompts; commands with spaces, single-token commands, and TUI
spinner titles must not be classified as idle. Documents the
benign edge case where bare absolute-path executables are treated
as prompts.

To enable direct testing, `WorktreeTerminalState.isLikelyIdleTitleByShape`
is promoted from `private func` to `static func`. The only call site
inside the type now uses `Self.` to dispatch.

Also adds `Assets.xcassets/CommandIcons/README.md` recording brand-
artwork sources (Simple Icons CC0 + Lobe Icons MIT), trademark
disclaimer, and the recipe for adding a new entry.

onevcat 43463ece c7d5bab7

+290 -2
+42
supacode/Assets.xcassets/CommandIcons/README.md
··· 1 + # Command Icons 2 + 3 + Brand artwork used by the auto-detected tab icon 4 + (`CommandIconMap` → `TabIconImage`). All SVGs ship as monochrome 5 + templates (`template-rendering-intent: "template"` + 6 + `preserves-vector-representation: true`) so they tint with the 7 + surrounding `foregroundStyle` and adapt to dark / light appearance 8 + without per-mode variants. 9 + 10 + ## Sources 11 + 12 + | Source | License | Imagesets | 13 + | ------ | ------- | --------- | 14 + | [Simple Icons](https://simpleicons.org/) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | AWS, Azure, Bun, Curl, Deno, Docker, Git, GitHub, Go, GoogleCloud, Gradle, Homebrew, Kubernetes, MySQL, Neovim, Node, Npm, Pnpm, Podman, PostgreSQL, Python, Rust, SQLite, Swift, Terraform, Tmux, TypeScript, Vim, VSCode, Xcode, Yarn, Gemini | 15 + | [Lobe Icons](https://github.com/lobehub/lobe-icons) | [MIT](https://github.com/lobehub/lobe-icons/blob/master/LICENSE) | Amp, ClaudeCode, Codex, GitHubCopilot, Kimi, OpenCode | 16 + 17 + `ClaudeCode` is sourced from the Lobe Icons `claude.svg` mark and 18 + re-authored as a single `fill-rule="evenodd"` path so the `>_` glyph 19 + renders as a native cutout under SwiftUI template tinting (the 20 + upstream two-path version relies on multi-colour `fill` that 21 + `Image(_:)` can't reproduce). 22 + 23 + ## Trademarks 24 + 25 + The image files are released under permissive licenses, but the 26 + **marks themselves remain the trademarks of their respective 27 + holders**. Inclusion here is for tool integration only — surfacing a 28 + brand alongside the matching CLI is a long-standing convention in 29 + terminal apps (iTerm2, Warp, Wezterm, …) and not an endorsement. 30 + Remove or replace any entry whose holder objects. 31 + 32 + ## Adding a new entry 33 + 34 + 1. Drop a single-colour SVG (use `currentColor` or a bare path) into 35 + `<Name>.imageset/`. 36 + 2. Add a `Contents.json` mirroring an existing imageset 37 + (`preserves-vector-representation: true` + 38 + `template-rendering-intent: "template"`). 39 + 3. Reference the asset in `CommandIconMap`: 40 + `TabIconSource(systemSymbol: "<sf-fallback>", assetName: "<Name>")`. 41 + 4. (Optional) Verify how it looks via 42 + **Debug → Icon Catalog** in a DEBUG build.
+5 -2
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 1452 1452 learnedIdleTitlesBySurface[surfaceId, default: []].insert(title) 1453 1453 } 1454 1454 // Drop idle prompts so they can't reach the mapping lookup. 1455 - if isLikelyIdleTitleByShape(title) { return } 1455 + if Self.isLikelyIdleTitleByShape(title) { return } 1456 1456 if learnedIdleTitlesBySurface[surfaceId]?.contains(title) == true { return } 1457 1457 guard let icon = CommandIconMap.iconForFirstToken(title) else { return } 1458 1458 applyResolvedIcon(icon, surfaceId: surfaceId, tabId: tabId) ··· 1481 1481 /// Real commands typically contain a space (program + args) or a 1482 1482 /// short single token (`ls`, `claude`, `vim`) that doesn't match 1483 1483 /// either shape, so the false-negative risk is small. 1484 - private func isLikelyIdleTitleByShape(_ title: String) -> Bool { 1484 + /// 1485 + /// Exposed (`internal static`) for direct unit testing — does not 1486 + /// touch instance state. 1487 + static func isLikelyIdleTitleByShape(_ title: String) -> Bool { 1485 1488 guard !title.contains(" ") else { return false } 1486 1489 if title.contains("@"), title.contains(":") || title.contains("/") { 1487 1490 return true
+94
supacodeTests/CommandIconMapTests.swift
··· 1 + import Testing 2 + 3 + @testable import supacode 4 + 5 + struct CommandIconMapTests { 6 + // MARK: - First-token resolution 7 + 8 + @Test func resolvesExactToken() throws { 9 + let icon = try #require(CommandIconMap.iconForFirstToken("git")) 10 + #expect(icon.systemSymbol == "arrow.triangle.branch") 11 + #expect(icon.assetName == "Git") 12 + } 13 + 14 + @Test func resolvesByFirstTokenWithArgs() { 15 + // "git status" should match the `git` entry, not look up "git status". 16 + #expect(CommandIconMap.iconForFirstToken("git status")?.assetName == "Git") 17 + #expect(CommandIconMap.iconForFirstToken("swift build --release")?.assetName == "Swift") 18 + #expect(CommandIconMap.iconForFirstToken("docker compose up -d")?.assetName == "Docker") 19 + } 20 + 21 + @Test func lookupIsCaseInsensitive() { 22 + #expect(CommandIconMap.iconForFirstToken("GIT")?.assetName == "Git") 23 + #expect(CommandIconMap.iconForFirstToken("Docker")?.assetName == "Docker") 24 + #expect(CommandIconMap.iconForFirstToken("CLAUDE")?.assetName == "ClaudeCode") 25 + } 26 + 27 + @Test func returnsNilForUnknownToken() { 28 + #expect(CommandIconMap.iconForFirstToken("never-heard-of-this-cli") == nil) 29 + #expect(CommandIconMap.iconForFirstToken("xyzzy") == nil) 30 + } 31 + 32 + @Test func returnsNilForEmptyTitle() { 33 + #expect(CommandIconMap.iconForFirstToken("") == nil) 34 + } 35 + 36 + @Test func handlesLeadingWhitespace() { 37 + // `split(omittingEmpty:)` skips the leading space so the first 38 + // real token still resolves. 39 + #expect(CommandIconMap.iconForFirstToken(" git status")?.assetName == "Git") 40 + } 41 + 42 + // MARK: - Aliases reuse the right asset 43 + 44 + @Test func packageManagerAliasesShareAssets() { 45 + // Runners share the icon of their parent package manager. 46 + #expect(CommandIconMap.iconForFirstToken("npx")?.assetName == "Npm") 47 + #expect(CommandIconMap.iconForFirstToken("bunx")?.assetName == "Bun") 48 + #expect(CommandIconMap.iconForFirstToken("pip")?.assetName == "Python") 49 + #expect(CommandIconMap.iconForFirstToken("pip3")?.assetName == "Python") 50 + } 51 + 52 + @Test func tuiFrontendsShareAssets() { 53 + // lazygit/lazydocker are TUI frontends — share the icon. 54 + #expect(CommandIconMap.iconForFirstToken("lazygit")?.assetName == "Git") 55 + #expect(CommandIconMap.iconForFirstToken("lazydocker")?.assetName == "Docker") 56 + } 57 + 58 + @Test func pythonAliasMapsToPython() { 59 + #expect(CommandIconMap.iconForFirstToken("python")?.assetName == "Python") 60 + #expect(CommandIconMap.iconForFirstToken("python3")?.assetName == "Python") 61 + } 62 + 63 + // MARK: - Coding agents 64 + 65 + @Test func codingAgentsResolved() { 66 + // Sample of the coding-agent set — they all share the sparkle SF 67 + // Symbol fallback, asset names match the imageset folders. 68 + #expect(CommandIconMap.iconForFirstToken("claude")?.assetName == "ClaudeCode") 69 + #expect(CommandIconMap.iconForFirstToken("codex")?.assetName == "Codex") 70 + #expect(CommandIconMap.iconForFirstToken("gemini")?.assetName == "Gemini") 71 + #expect(CommandIconMap.iconForFirstToken("copilot")?.assetName == "GitHubCopilot") 72 + // aider/droid have no brand asset — sparkle fallback only. 73 + #expect(CommandIconMap.iconForFirstToken("aider")?.systemSymbol == "sparkle") 74 + #expect(CommandIconMap.iconForFirstToken("aider")?.assetName == nil) 75 + #expect(CommandIconMap.iconForFirstToken("droid")?.systemSymbol == "sparkle") 76 + } 77 + 78 + // MARK: - Debug catalog 79 + 80 + @Test func debugAllEntriesIsSorted() { 81 + let tokens = CommandIconMap.debugAllEntries.map(\.token) 82 + #expect(tokens == tokens.sorted()) 83 + } 84 + 85 + @Test func debugAllEntriesCoversWellKnownTokens() { 86 + let tokens = Set(CommandIconMap.debugAllEntries.map(\.token)) 87 + // Spot-check that the debug surface actually exposes the tokens 88 + // a user is most likely to hunt for. 89 + let mustHave: Set<String> = [ 90 + "git", "docker", "claude", "vim", "ssh", "npm", "swift", 91 + ] 92 + #expect(mustHave.isSubset(of: tokens)) 93 + } 94 + }
+79
supacodeTests/IconDetectorIdleHeuristicTests.swift
··· 1 + import Testing 2 + 3 + @testable import supacode 4 + 5 + /// Pure-shape detection of the shell's idle prompt 6 + /// (`isLikelyIdleTitleByShape`). The bootstrap filter that runs 7 + /// before the per-surface learner has memorised the prompt at least 8 + /// once. 9 + struct IconDetectorIdleHeuristicTests { 10 + // MARK: - Idle prompt shapes 11 + 12 + @Test func detectsUserAtHostWithColonPath() { 13 + #expect(WorktreeTerminalState.isLikelyIdleTitleByShape("onevcat@Mac:~/Sync/github/YiTong")) 14 + } 15 + 16 + @Test func detectsUserAtHostWithSlashOnly() { 17 + #expect(WorktreeTerminalState.isLikelyIdleTitleByShape("onevcat@Mac:/usr/local/etc")) 18 + } 19 + 20 + @Test func detectsTildePath() { 21 + #expect(WorktreeTerminalState.isLikelyIdleTitleByShape("~/Sync/github")) 22 + } 23 + 24 + @Test func detectsAbsolutePath() { 25 + #expect(WorktreeTerminalState.isLikelyIdleTitleByShape("/usr/local/bin")) 26 + } 27 + 28 + @Test func detectsTruncatedPathWithEllipsis() { 29 + // zsh's "compact path" renders as `…/Sync/github/YiTong`. 30 + #expect(WorktreeTerminalState.isLikelyIdleTitleByShape("…/Sync/github/YiTong")) 31 + } 32 + 33 + // MARK: - Real commands should not be flagged 34 + 35 + @Test func commandWithSpaceIsNotIdle() { 36 + // Anything with a space is treated as a real command (program + 37 + // args) — this is the primary discriminator. 38 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("git status")) 39 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("vim file.swift")) 40 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("docker compose up")) 41 + } 42 + 43 + @Test func barCommandTokenIsNotIdle() { 44 + // Single-token commands without `@`, `~`, `/`, or `…` are real 45 + // commands. 46 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("claude")) 47 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("vim")) 48 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("npm")) 49 + } 50 + 51 + @Test func tuiTitleIsNotIdle() { 52 + // TUI tools that rewrite their own title (claude → spinner glyphs) 53 + // contain spaces and should still be classified as commands. 54 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("✳ Claude Code")) 55 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("⠐ Claude Code")) 56 + } 57 + 58 + @Test func emptyTitleIsNotIdle() { 59 + // Empty handled by the caller; the heuristic itself returns false 60 + // (no `@`, no leading `~`/`/`/`…`). 61 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("")) 62 + } 63 + 64 + // MARK: - Edge cases 65 + 66 + @Test func atSymbolWithoutPathSeparatorIsNotIdle() { 67 + // `git@github.com` would be a typical SSH remote, not an idle 68 + // prompt. Without `:` or `/` it's not classified as idle. 69 + #expect(!WorktreeTerminalState.isLikelyIdleTitleByShape("git@github.com")) 70 + } 71 + 72 + @Test func absolutePathExecutableIsClassifiedAsIdle() { 73 + // Documented limitation: `/usr/bin/python3` (rare invocation 74 + // form) shape-matches as a "path" prompt and gets skipped. The 75 + // tradeoff is fine — typical use is `python3`, not the absolute 76 + // path. 77 + #expect(WorktreeTerminalState.isLikelyIdleTitleByShape("/usr/bin/python3")) 78 + } 79 + }
+70
supacodeTests/TabIconSourceTests.swift
··· 1 + import Testing 2 + 3 + @testable import supacode 4 + 5 + struct TabIconSourceTests { 6 + // MARK: - storageString encoding 7 + 8 + @Test func sfSymbolOnlySerialisesBare() { 9 + // SF-Symbol-only entries serialise as the bare symbol name so 10 + // the existing IconPicker storage path keeps working unchanged. 11 + let icon = TabIconSource(systemSymbol: "terminal") 12 + #expect(icon.storageString == "terminal") 13 + } 14 + 15 + @Test func assetEntrySerialisesWithMarker() { 16 + // Asset-bearing entries get the `@asset:` prefix the renderer 17 + // parses via `ResolvedTabIcon`. 18 + let icon = TabIconSource(systemSymbol: "shippingbox", assetName: "Docker") 19 + #expect(icon.storageString == "@asset:Docker") 20 + } 21 + 22 + @Test func assetEntryOmitsSystemSymbolFromStorage() { 23 + // `systemSymbol` stays only as a fallback for renderers that 24 + // can't resolve the asset; storage carries the asset. 25 + let icon = TabIconSource(systemSymbol: "sparkle", assetName: "ClaudeCode") 26 + #expect(icon.storageString == "@asset:ClaudeCode") 27 + #expect(!icon.storageString.contains("sparkle")) 28 + } 29 + 30 + // MARK: - ResolvedTabIcon parsing 31 + 32 + @Test func parsesBareStringAsSystemSymbol() { 33 + let resolved = ResolvedTabIcon.parse("terminal") 34 + #expect(resolved == .systemSymbol("terminal")) 35 + } 36 + 37 + @Test func parsesAssetMarker() { 38 + let resolved = ResolvedTabIcon.parse("@asset:Docker") 39 + #expect(resolved == .asset(name: "Docker")) 40 + } 41 + 42 + @Test func parsesAssetMarkerWithSpaces() { 43 + // Asset names can contain spaces (e.g. "Visual Studio Code"), so 44 + // the parser must keep everything after the marker prefix intact. 45 + let resolved = ResolvedTabIcon.parse("@asset:Visual Studio Code") 46 + #expect(resolved == .asset(name: "Visual Studio Code")) 47 + } 48 + 49 + @Test func sfSymbolStringWithColonStaysSymbol() { 50 + // Edge: SF Symbol names never start with `@asset:`, so a literal 51 + // colon-bearing symbol (none today, but defensive) doesn't trip 52 + // the parser. 53 + let resolved = ResolvedTabIcon.parse("foo:bar") 54 + #expect(resolved == .systemSymbol("foo:bar")) 55 + } 56 + 57 + // MARK: - Round-trip 58 + 59 + @Test func sfSymbolRoundTrip() { 60 + let source = TabIconSource(systemSymbol: "hammer") 61 + let parsed = ResolvedTabIcon.parse(source.storageString) 62 + #expect(parsed == .systemSymbol("hammer")) 63 + } 64 + 65 + @Test func assetRoundTrip() { 66 + let source = TabIconSource(systemSymbol: "shippingbox", assetName: "Npm") 67 + let parsed = ResolvedTabIcon.parse(source.storageString) 68 + #expect(parsed == .asset(name: "Npm")) 69 + } 70 + }