diff --git a/Assets.xcassets/AgentIcons/ATTRIBUTIONS.md b/Assets.xcassets/AgentIcons/ATTRIBUTIONS.md new file mode 100644 index 000000000..ec537d481 --- /dev/null +++ b/Assets.xcassets/AgentIcons/ATTRIBUTIONS.md @@ -0,0 +1,46 @@ +# Agent tab icons + +The SVGs in this folder are the agent logos shown on terminal tabs when Programa detects a coding agent in that terminal. + +- **Source:** https://github.com/lobehub/lobe-icons, `packages/static-svg/icons/` +- **Pinned commit:** `a94750e3f5f8fc33757b839d85030e742284e43a` +- **License:** MIT, Copyright (c) 2023 LobeHub (text below) + +| Asset | Lobe Icons file | +|---|---| +| `AgentIconClaudeCode` | `claudecode-color.svg` | +| `AgentIconCodex` | `codex-color.svg` | +| `AgentIconCopilot` | `githubcopilot.svg` | +| `AgentIconCursor` | `cursor.svg` | +| `AgentIconGeminiCLI` | `geminicli-color.svg` | +| `AgentIconOpenCode` | `opencode.svg` | + +Aider has no icon in Lobe Icons, so Aider terminals keep the terminal symbol. + +Local changes to each file: the `` element and the inline `style` attribute are removed, and `width`/`height` of `1em` become `24`. The monochrome logos (Copilot, Cursor, OpenCode) replace `fill="currentColor"` with `#000000` for the light appearance and a second file with `#FFFFFF` for the dark appearance. Path data is re-serialized with spaces between values and explicit arc flags, because Xcode's SVG compiler rejects the compact flag form; the geometry is identical. + +The logos are trademarks of their respective owners and are used here only to identify the tool running in a tab. + +## Lobe Icons license + +MIT License + +Copyright (c) 2023 LobeHub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Assets.xcassets/AgentIcons/AgentIconClaudeCode.imageset/AgentIconClaudeCode.svg b/Assets.xcassets/AgentIcons/AgentIconClaudeCode.imageset/AgentIconClaudeCode.svg new file mode 100644 index 000000000..c94a619ea --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconClaudeCode.imageset/AgentIconClaudeCode.svg @@ -0,0 +1 @@ +<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z" fill="#D97757" fill-rule="evenodd"></path></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconClaudeCode.imageset/Contents.json b/Assets.xcassets/AgentIcons/AgentIconClaudeCode.imageset/Contents.json new file mode 100644 index 000000000..9a7bca1d8 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconClaudeCode.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images": [ + { + "filename": "AgentIconClaudeCode.svg", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + }, + "properties": { + "preserves-vector-representation": true + } +} diff --git a/Assets.xcassets/AgentIcons/AgentIconCodex.imageset/AgentIconCodex.svg b/Assets.xcassets/AgentIcons/AgentIconCodex.imageset/AgentIconCodex.svg new file mode 100644 index 000000000..a1b1e01e8 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCodex.imageset/AgentIconCodex.svg @@ -0,0 +1 @@ +<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19.503 0H4.496A4.496 4.496 0 0 0 0 4.496v15.007A4.496 4.496 0 0 0 4.496 24h15.007A4.496 4.496 0 0 0 24 19.503V4.496A4.496 4.496 0 0 0 19.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 0 1 2.285 -.312c1 .115 1.891 .54 2.673 1.275 .01 .01 .024 .017 .037 .021a.09 .09 0 0 0 .043 0 4.55 4.55 0 0 1 3.046 .275l.047 .022 .116 .057a4.581 4.581 0 0 1 2.188 2.399c.209 .51 .313 1.041 .315 1.595a4.24 4.24 0 0 1 -.134 1.223 .123 .123 0 0 0 .03 .115c.594 .607 .988 1.33 1.183 2.17 .289 1.425 -.007 2.71 -.887 3.854l-.136 .166a4.548 4.548 0 0 1 -2.201 1.388 .123 .123 0 0 0 -.081 .076c-.191 .551 -.383 1.023 -.74 1.494 -.9 1.187 -2.222 1.846 -3.711 1.838 -1.187 -.006 -2.239 -.44 -3.157 -1.302a.107 .107 0 0 0 -.105 -.024c-.388 .125 -.78 .143 -1.204 .138a4.441 4.441 0 0 1 -1.945 -.466 4.544 4.544 0 0 1 -1.61 -1.335c-.152 -.202 -.303 -.392 -.414 -.617a5.81 5.81 0 0 1 -.37 -.961 4.582 4.582 0 0 1 -.014 -2.298 .124 .124 0 0 0 .006 -.056 .085 .085 0 0 0 -.027 -.048 4.467 4.467 0 0 1 -1.034 -1.651 3.896 3.896 0 0 1 -.251 -1.192 5.189 5.189 0 0 1 .141 -1.6c.337 -1.112 .982 -1.985 1.933 -2.618 .212 -.141 .413 -.251 .601 -.33 .215 -.089 .43 -.164 .646 -.227a.098 .098 0 0 0 .065 -.066 4.51 4.51 0 0 1 .829 -1.615 4.535 4.535 0 0 1 1.837 -1.388zm3.482 10.565a.637 .637 0 0 0 0 1.272h3.636a.637 .637 0 1 0 0 -1.272h-3.636zM8.462 9.23a.637 .637 0 0 0 -1.106 .631l1.272 2.224 -1.266 2.136a.636 .636 0 1 0 1.095 .649l1.454 -2.455a.636 .636 0 0 0 .005 -.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconCodex.imageset/Contents.json b/Assets.xcassets/AgentIcons/AgentIconCodex.imageset/Contents.json new file mode 100644 index 000000000..5cd2a506f --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCodex.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images": [ + { + "filename": "AgentIconCodex.svg", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + }, + "properties": { + "preserves-vector-representation": true + } +} diff --git a/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/AgentIconCopilot-dark.svg b/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/AgentIconCopilot-dark.svg new file mode 100644 index 000000000..1524d9b89 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/AgentIconCopilot-dark.svg @@ -0,0 +1 @@ +<svg fill="#FFFFFF" fill-rule="evenodd" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817 .622 0 1.2 .135 1.592 .654l.73 .964c.21 .278 .323 .61 .323 .955v2.62c0 .339 -.173 .669 -.453 .868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0 -9.205 -2.583 -11.547 -4.258 -.28 -.2 -.452 -.53 -.453 -.868v-2.62c0 -.345 .113 -.679 .321 -.956l.73 -.963c.392 -.517 .974 -.654 1.593 -.654l.029 -.297c.25 -2.446 .81 -4.213 2.082 -5.52 2.461 -2.54 5.71 -2.851 7.146 -2.864h.198c1.436 .013 4.685 .323 7.146 2.864zm-7.244 4.328c-.284 0 -.613 .016 -.962 .05 -.123 .447 -.305 .85 -.57 1.108 -1.05 1.023 -2.316 1.18 -2.994 1.18 -.638 0 -1.306 -.13 -1.851 -.464 -.516 .165 -1.012 .403 -1.044 .996a65.882 65.882 0 0 0 -.063 2.884l-.002 .48c-.002 .563 -.005 1.126 -.013 1.69 .002 .326 .204 .63 .51 .765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504 -.555 6.985 -1.657a.854 .854 0 0 0 .51 -.766c.03 -1.682 .006 -3.372 -.076 -5.053 -.031 -.596 -.528 -.83 -1.046 -.996 -.546 .333 -1.212 .464 -1.85 .464 -.677 0 -1.942 -.157 -2.993 -1.18 -.266 -.258 -.447 -.661 -.57 -1.108 -.32 -.032 -.64 -.049 -.96 -.05zm-2.525 4.013c.539 0 .976 .426 .976 .95v1.753c0 .525 -.437 .95 -.976 .95a.964 .964 0 0 1 -.976 -.95v-1.752c0 -.525 .437 -.951 .976 -.951zm5 0c.539 0 .976 .426 .976 .95v1.753c0 .525 -.437 .95 -.976 .95a.964 .964 0 0 1 -.976 -.95v-1.752c0 -.525 .437 -.951 .976 -.951zM7.635 5.087c-1.05 .102 -1.935 .438 -2.385 .906 -.975 1.037 -.765 3.668 -.21 4.224 .405 .394 1.17 .657 1.995 .657h.09c.649 -.013 1.785 -.176 2.73 -1.11 .435 -.41 .705 -1.433 .675 -2.47 -.03 -.834 -.27 -1.52 -.63 -1.813 -.39 -.336 -1.275 -.482 -2.265 -.394zm6.465 .394c-.36 .292 -.6 .98 -.63 1.813 -.03 1.037 .24 2.06 .675 2.47 .968 .957 2.136 1.104 2.776 1.11h.044c.825 0 1.59 -.263 1.995 -.657 .555 -.556 .765 -3.187 -.21 -4.224 -.45 -.468 -1.335 -.804 -2.385 -.906 -.99 -.088 -1.875 .058 -2.265 .394zM12 7.615c-.24 0 -.525 .015 -.84 .044 .03 .16 .045 .336 .06 .526l-.001 .159a2.94 2.94 0 0 1 -.014 .25c.225 -.022 .425 -.027 .612 -.028h.366c.187 0 .387 .006 .612 .028 -.015 -.146 -.015 -.277 -.015 -.409 .015 -.19 .03 -.365 .06 -.526a9.29 9.29 0 0 0 -.84 -.044z"></path></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/AgentIconCopilot.svg b/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/AgentIconCopilot.svg new file mode 100644 index 000000000..de65e37a6 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/AgentIconCopilot.svg @@ -0,0 +1 @@ +<svg fill="#000000" fill-rule="evenodd" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817 .622 0 1.2 .135 1.592 .654l.73 .964c.21 .278 .323 .61 .323 .955v2.62c0 .339 -.173 .669 -.453 .868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0 -9.205 -2.583 -11.547 -4.258 -.28 -.2 -.452 -.53 -.453 -.868v-2.62c0 -.345 .113 -.679 .321 -.956l.73 -.963c.392 -.517 .974 -.654 1.593 -.654l.029 -.297c.25 -2.446 .81 -4.213 2.082 -5.52 2.461 -2.54 5.71 -2.851 7.146 -2.864h.198c1.436 .013 4.685 .323 7.146 2.864zm-7.244 4.328c-.284 0 -.613 .016 -.962 .05 -.123 .447 -.305 .85 -.57 1.108 -1.05 1.023 -2.316 1.18 -2.994 1.18 -.638 0 -1.306 -.13 -1.851 -.464 -.516 .165 -1.012 .403 -1.044 .996a65.882 65.882 0 0 0 -.063 2.884l-.002 .48c-.002 .563 -.005 1.126 -.013 1.69 .002 .326 .204 .63 .51 .765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504 -.555 6.985 -1.657a.854 .854 0 0 0 .51 -.766c.03 -1.682 .006 -3.372 -.076 -5.053 -.031 -.596 -.528 -.83 -1.046 -.996 -.546 .333 -1.212 .464 -1.85 .464 -.677 0 -1.942 -.157 -2.993 -1.18 -.266 -.258 -.447 -.661 -.57 -1.108 -.32 -.032 -.64 -.049 -.96 -.05zm-2.525 4.013c.539 0 .976 .426 .976 .95v1.753c0 .525 -.437 .95 -.976 .95a.964 .964 0 0 1 -.976 -.95v-1.752c0 -.525 .437 -.951 .976 -.951zm5 0c.539 0 .976 .426 .976 .95v1.753c0 .525 -.437 .95 -.976 .95a.964 .964 0 0 1 -.976 -.95v-1.752c0 -.525 .437 -.951 .976 -.951zM7.635 5.087c-1.05 .102 -1.935 .438 -2.385 .906 -.975 1.037 -.765 3.668 -.21 4.224 .405 .394 1.17 .657 1.995 .657h.09c.649 -.013 1.785 -.176 2.73 -1.11 .435 -.41 .705 -1.433 .675 -2.47 -.03 -.834 -.27 -1.52 -.63 -1.813 -.39 -.336 -1.275 -.482 -2.265 -.394zm6.465 .394c-.36 .292 -.6 .98 -.63 1.813 -.03 1.037 .24 2.06 .675 2.47 .968 .957 2.136 1.104 2.776 1.11h.044c.825 0 1.59 -.263 1.995 -.657 .555 -.556 .765 -3.187 -.21 -4.224 -.45 -.468 -1.335 -.804 -2.385 -.906 -.99 -.088 -1.875 .058 -2.265 .394zM12 7.615c-.24 0 -.525 .015 -.84 .044 .03 .16 .045 .336 .06 .526l-.001 .159a2.94 2.94 0 0 1 -.014 .25c.225 -.022 .425 -.027 .612 -.028h.366c.187 0 .387 .006 .612 .028 -.015 -.146 -.015 -.277 -.015 -.409 .015 -.19 .03 -.365 .06 -.526a9.29 9.29 0 0 0 -.84 -.044z"></path></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/Contents.json b/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/Contents.json new file mode 100644 index 000000000..a253ae158 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCopilot.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images": [ + { + "filename": "AgentIconCopilot.svg", + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "filename": "AgentIconCopilot-dark.svg", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + }, + "properties": { + "preserves-vector-representation": true + } +} diff --git a/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/AgentIconCursor-dark.svg b/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/AgentIconCursor-dark.svg new file mode 100644 index 000000000..9b5db6a1c --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/AgentIconCursor-dark.svg @@ -0,0 +1 @@ +<svg fill="#FFFFFF" fill-rule="evenodd" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M22.106 5.68L12.5 .135a.998 .998 0 0 0 -.998 0L1.893 5.68a.84 .84 0 0 0 -.419 .726v11.186c0 .3 .16 .577 .42 .727l9.607 5.547a.999 .999 0 0 0 .998 0l9.608 -5.547a.84 .84 0 0 0 .42 -.727V6.407a.84 .84 0 0 0 -.42 -.726zm-.603 1.176L12.228 22.92c-.063 .108 -.228 .064 -.228 -.061V12.34a.59 .59 0 0 0 -.295 -.51l-9.11 -5.26c-.107 -.062 -.063 -.228 .062 -.228h18.55c.264 0 .428 .286 .296 .514z"></path></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/AgentIconCursor.svg b/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/AgentIconCursor.svg new file mode 100644 index 000000000..86bcfd46a --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/AgentIconCursor.svg @@ -0,0 +1 @@ +<svg fill="#000000" fill-rule="evenodd" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M22.106 5.68L12.5 .135a.998 .998 0 0 0 -.998 0L1.893 5.68a.84 .84 0 0 0 -.419 .726v11.186c0 .3 .16 .577 .42 .727l9.607 5.547a.999 .999 0 0 0 .998 0l9.608 -5.547a.84 .84 0 0 0 .42 -.727V6.407a.84 .84 0 0 0 -.42 -.726zm-.603 1.176L12.228 22.92c-.063 .108 -.228 .064 -.228 -.061V12.34a.59 .59 0 0 0 -.295 -.51l-9.11 -5.26c-.107 -.062 -.063 -.228 .062 -.228h18.55c.264 0 .428 .286 .296 .514z"></path></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/Contents.json b/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/Contents.json new file mode 100644 index 000000000..4d9fe733f --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconCursor.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images": [ + { + "filename": "AgentIconCursor.svg", + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "filename": "AgentIconCursor-dark.svg", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + }, + "properties": { + "preserves-vector-representation": true + } +} diff --git a/Assets.xcassets/AgentIcons/AgentIconGeminiCLI.imageset/AgentIconGeminiCLI.svg b/Assets.xcassets/AgentIcons/AgentIconGeminiCLI.imageset/AgentIconGeminiCLI.svg new file mode 100644 index 000000000..389c4449f --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconGeminiCLI.imageset/AgentIconGeminiCLI.svg @@ -0,0 +1 @@ +<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 4.391A4.391 4.391 0 0 1 4.391 0h15.217A4.391 4.391 0 0 1 24 4.391v15.217A4.391 4.391 0 0 1 19.608 24H4.391A4.391 4.391 0 0 1 0 19.608V4.391z" fill="url(#lobe-icons-gemini-cli-_R_0_)"></path><path clip-rule="evenodd" d="M19.74 1.444a2.816 2.816 0 0 1 2.816 2.816v15.48a2.816 2.816 0 0 1 -2.816 2.816H4.26a2.816 2.816 0 0 1 -2.816 -2.816V4.26A2.816 2.816 0 0 1 4.26 1.444h15.48zM7.236 8.564l7.752 3.728 -7.752 3.727v2.802l9.557 -4.596v-3.866L7.236 5.763v2.801z" fill="#1E1E2E" fill-rule="evenodd"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-cli-_R_0_" x1="24" x2="0" y1="6.587" y2="16.494"><stop stop-color="#EE4D5D"></stop><stop offset=".328" stop-color="#B381DD"></stop><stop offset=".476" stop-color="#207CFE"></stop></linearGradient></defs></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconGeminiCLI.imageset/Contents.json b/Assets.xcassets/AgentIcons/AgentIconGeminiCLI.imageset/Contents.json new file mode 100644 index 000000000..59ff18681 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconGeminiCLI.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images": [ + { + "filename": "AgentIconGeminiCLI.svg", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + }, + "properties": { + "preserves-vector-representation": true + } +} diff --git a/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/AgentIconOpenCode-dark.svg b/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/AgentIconOpenCode-dark.svg new file mode 100644 index 000000000..359e358cb --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/AgentIconOpenCode-dark.svg @@ -0,0 +1 @@ +<svg fill="#FFFFFF" fill-rule="evenodd" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M16 6H8v12h8V6zm4 16H4V2h16v20z"></path></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/AgentIconOpenCode.svg b/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/AgentIconOpenCode.svg new file mode 100644 index 000000000..9adb15036 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/AgentIconOpenCode.svg @@ -0,0 +1 @@ +<svg fill="#000000" fill-rule="evenodd" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M16 6H8v12h8V6zm4 16H4V2h16v20z"></path></svg> diff --git a/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/Contents.json b/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/Contents.json new file mode 100644 index 000000000..8ddd64dd8 --- /dev/null +++ b/Assets.xcassets/AgentIcons/AgentIconOpenCode.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images": [ + { + "filename": "AgentIconOpenCode.svg", + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "filename": "AgentIconOpenCode-dark.svg", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + }, + "properties": { + "preserves-vector-representation": true + } +} diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 61d0099bc..724158be8 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ NRSP0076A1B2C3D4E5F60719 /* Workspace+Surfaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRSP0075A1B2C3D4E5F60719 /* Workspace+Surfaces.swift */; }; NRSP0074A1B2C3D4E5F60719 /* Workspace+SidebarTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRSP0073A1B2C3D4E5F60719 /* Workspace+SidebarTelemetry.swift */; }; AGST0001A1B2C3D4E5F60719 /* AgentActivityState.swift in Sources */ = {isa = PBXBuildFile; fileRef = AGST0002A1B2C3D4E5F60719 /* AgentActivityState.swift */; }; + TTAI0001A1B2C3D4E5F60719 /* TerminalTabAgentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = TTAI0002A1B2C3D4E5F60719 /* TerminalTabAgentIcon.swift */; }; 7501A0000000000000000003 /* GhosttyApp+ProgramStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7501A0000000000000000004 /* GhosttyApp+ProgramStatus.swift */; }; 7501A0000000000000000001 /* ProgramStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7501A0000000000000000002 /* ProgramStatus.swift */; }; 7501A0000000000000000007 /* TerminalController+SurfaceWaitProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7501A0000000000000000008 /* TerminalController+SurfaceWaitProgram.swift */; }; @@ -417,6 +418,7 @@ NRSP0075A1B2C3D4E5F60719 /* Workspace+Surfaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+Surfaces.swift"; sourceTree = "<group>"; }; NRSP0073A1B2C3D4E5F60719 /* Workspace+SidebarTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+SidebarTelemetry.swift"; sourceTree = "<group>"; }; AGST0002A1B2C3D4E5F60719 /* AgentActivityState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AgentActivityState.swift"; sourceTree = "<group>"; }; + TTAI0002A1B2C3D4E5F60719 /* TerminalTabAgentIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalTabAgentIcon.swift"; sourceTree = "<group>"; }; 7501A0000000000000000002 /* ProgramStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramStatus.swift; sourceTree = "<group>"; }; 7501A0000000000000000008 /* TerminalController+SurfaceWaitProgram.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+SurfaceWaitProgram.swift"; sourceTree = "<group>"; }; 7501A0000000000000000004 /* GhosttyApp+ProgramStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyApp+ProgramStatus.swift"; sourceTree = "<group>"; }; @@ -865,6 +867,7 @@ NRSP0075A1B2C3D4E5F60719 /* Workspace+Surfaces.swift */, NRSP0073A1B2C3D4E5F60719 /* Workspace+SidebarTelemetry.swift */, AGST0002A1B2C3D4E5F60719 /* AgentActivityState.swift */, + TTAI0002A1B2C3D4E5F60719 /* TerminalTabAgentIcon.swift */, 7501A0000000000000000002 /* ProgramStatus.swift */, 7501A0000000000000000008 /* TerminalController+SurfaceWaitProgram.swift */, 7501A0000000000000000004 /* GhosttyApp+ProgramStatus.swift */, @@ -1398,6 +1401,7 @@ NRSP0076A1B2C3D4E5F60719 /* Workspace+Surfaces.swift in Sources */, NRSP0074A1B2C3D4E5F60719 /* Workspace+SidebarTelemetry.swift in Sources */, AGST0001A1B2C3D4E5F60719 /* AgentActivityState.swift in Sources */, + TTAI0001A1B2C3D4E5F60719 /* TerminalTabAgentIcon.swift in Sources */, 7501A0000000000000000001 /* ProgramStatus.swift in Sources */, 7501A0000000000000000007 /* TerminalController+SurfaceWaitProgram.swift in Sources */, 7501A0000000000000000003 /* GhosttyApp+ProgramStatus.swift in Sources */, diff --git a/Sources/AgentActivityState.swift b/Sources/AgentActivityState.swift index 3fe9a4594..84abf3c3e 100644 --- a/Sources/AgentActivityState.swift +++ b/Sources/AgentActivityState.swift @@ -85,6 +85,9 @@ struct AgentPresence: Equatable, Sendable { var source: AgentStateSource var lastEventAt: Date var sessionKey: AgentSessionKey? + /// Manifest id of the agent the screen-manifest engine recognized (`.inferred` source only; + /// inferred reports carry no session key, so this is the only record of who the agent is). + var inferredAgent: String? /// The root record's state when `source == .program`; `state` holds its legacy projection /// (done and error both read as `.idle`). Nil for every other source. var programState: ProgramStatusState? diff --git a/Sources/AgentScreenDetectionEngine.swift b/Sources/AgentScreenDetectionEngine.swift index 3f5e7c0ee..29ba664d5 100644 --- a/Sources/AgentScreenDetectionEngine.swift +++ b/Sources/AgentScreenDetectionEngine.swift @@ -354,7 +354,8 @@ final class AgentScreenDetectionEngine: @unchecked Sendable { workspace.updatePanelAgentState( panelId: surfaceId, state: resolvedState, - source: .inferred + source: .inferred, + inferredAgent: manifest.agent ) } } diff --git a/Sources/TerminalTabAgentIcon.swift b/Sources/TerminalTabAgentIcon.swift new file mode 100644 index 000000000..a74fe5cac --- /dev/null +++ b/Sources/TerminalTabAgentIcon.swift @@ -0,0 +1,76 @@ +// Agent logos on terminal tabs. A terminal whose panel has agent presence shows that agent's +// logo (asset catalog `Assets.xcassets/AgentIcons`) instead of the terminal symbol; the logo +// goes away when the presence clears. Both tab renderers (the SwiftUI bar in Bonsplit and the +// AppKit glass bar in WindowPaneChromePortal) read `Tab.iconAsset`. + +import Bonsplit +import Foundation + +enum TerminalTabAgentIconResolver { + /// Detected agent identifier -> asset name. Identifiers are the screen-manifest ids + /// (`Resources/AgentDetection/*.json`), the hook provider names, and the short names an agent + /// goes by in its own CLI. Aider is absent because Lobe Icons ships no Aider mark. + private static let assetNamesByIdentifier: [String: String] = [ + "claude-code": "AgentIconClaudeCode", + "claude": "AgentIconClaudeCode", + "codex": "AgentIconCodex", + "copilot-cli": "AgentIconCopilot", + "copilot": "AgentIconCopilot", + "github-copilot": "AgentIconCopilot", + "cursor-agent": "AgentIconCursor", + "cursor": "AgentIconCursor", + "gemini-cli": "AgentIconGeminiCLI", + "gemini": "AgentIconGeminiCLI", + "opencode": "AgentIconOpenCode", + ] + + static func assetName(forAgent identifier: String) -> String? { + let normalized = identifier.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return assetNamesByIdentifier[normalized] + } +} + +extension Workspace { + /// The logo for one terminal panel's current agent, or nil when no agent is present or the + /// agent has no logo. Identity comes from the first source that names a known agent: the + /// hook session key, the screen-manifest id, then the OSC 7501 root record's `app`. + func terminalTabAgentIconAsset(forPanelId panelId: UUID) -> String? { + guard let presence = panelAgentPresence[panelId] else { return nil } + let identifiers = [ + presence.sessionKey?.provider, + presence.inferredAgent, + presence.source == .program ? terminalPanel(for: panelId)?.programStatus.root?.app : nil, + ] + return identifiers.lazy.compactMap { $0 }.compactMap(TerminalTabAgentIconResolver.assetName(forAgent:)).first + } + + /// Reconciles one terminal tab's logo with its panel's agent presence. No-op when the tab + /// already shows the right asset. + func syncTerminalTabAgentIconAsset(forPanelId panelId: UUID) { + guard panels[panelId] is TerminalPanel, + let tabId = surfaceIdFromPanelId(panelId), + let tab = bonsplitController.tab(tabId) else { return } + let asset = terminalTabAgentIconAsset(forPanelId: panelId) +#if DEBUG + if let presence = panelAgentPresence[panelId] { + let app = terminalPanel(for: panelId)?.programStatus.root?.app + dlog( + "tab.agentIcon panel=\(panelId.uuidString.prefix(5)) source=\(presence.source) " + + "provider=\(presence.sessionKey?.provider ?? "nil") inferred=\(presence.inferredAgent ?? "nil") " + + "app=\(app ?? "nil") asset=\(asset ?? "nil")" + ) + } +#endif + guard tab.iconAsset != asset else { return } + bonsplitController.updateTab(tabId, iconAsset: .some(asset)) + } + + /// Runs after any write to `panelAgentPresence`, whichever path made it (hook, screen + /// inference, OSC 7501, sweep, detach/attach, prune). + func syncTerminalTabAgentIconAssets(previous: [UUID: AgentPresence]) { + for panelId in Set(previous.keys).union(panelAgentPresence.keys) + where previous[panelId] != panelAgentPresence[panelId] { + syncTerminalTabAgentIconAsset(forPanelId: panelId) + } + } +} diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index fd918bd5d..f1c5c1488 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -1206,6 +1206,8 @@ private final class NativeTabPillControl: NSControl, NSMenuDelegate, NSDraggingS closeButton.contentTintColor = .secondaryLabelColor if let data = tab.iconImageData, let image = NSImage(data: data) { iconView.image = image + } else if let asset = tab.iconAsset, let image = NSImage(named: asset) { + iconView.image = image } else if let icon = tab.icon { // Unconfigured symbols render at the full 16pt slot; boxy filled // glyphs like terminal.fill then dominate the pill. Match the diff --git a/Sources/Workspace+Bonsplit.swift b/Sources/Workspace+Bonsplit.swift index 17f5eb11a..38f8083d6 100644 --- a/Sources/Workspace+Bonsplit.swift +++ b/Sources/Workspace+Bonsplit.swift @@ -855,6 +855,7 @@ extension Workspace: @preconcurrency BonsplitDelegate { title: replacementPanel.displayTitle, icon: .some(replacementPanel.displayIcon), iconImageData: .some(nil), + iconAsset: .some(nil), kind: .some(SurfaceKind.terminal), hasCustomTitle: false, isDirty: replacementPanel.isDirty, diff --git a/Sources/Workspace+SidebarTelemetry.swift b/Sources/Workspace+SidebarTelemetry.swift index abbef6130..f429cfe33 100644 --- a/Sources/Workspace+SidebarTelemetry.swift +++ b/Sources/Workspace+SidebarTelemetry.swift @@ -243,6 +243,7 @@ extension Workspace { state: AgentActivityState, source: AgentStateSource = .hooks, sessionKey: AgentSessionKey? = nil, + inferredAgent: String? = nil, programState: ProgramStatusState? = nil, at now: Date = Date() ) { @@ -271,7 +272,10 @@ extension Workspace { resolvedSessionKey = sessionKey ?? current?.sessionKey } + let resolvedInferredAgent = source == .inferred ? inferredAgent : nil + guard current?.state != state || current?.source != source || current?.sessionKey != resolvedSessionKey + || current?.inferredAgent != resolvedInferredAgent || current?.programState != programState else { // Same state from the same writer: only the liveness clock moves. Without this a // long-running agent that keeps reporting `working` would read as stale at the @@ -285,6 +289,7 @@ extension Workspace { source: source, lastEventAt: now, sessionKey: resolvedSessionKey, + inferredAgent: resolvedInferredAgent, programState: programState ) // Event-driven half of surface.wait's `agent_state` condition (#166 task 2) and diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 4cfd10061..8ea4646ef 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -142,7 +142,9 @@ final class Workspace: Identifiable, ObservableObject { /// by the agent-state-unification plan). This is the stored source of truth; `panelAgentStates` /// and `panelAgentStateSources` below are derived, read-only mirrors kept for the existing /// wire shapes (`agent_state`/`agent_state_source`) and call sites. See AgentActivityState.swift. - @Published var panelAgentPresence: [UUID: AgentPresence] = [:] + @Published var panelAgentPresence: [UUID: AgentPresence] = [:] { + didSet { syncTerminalTabAgentIconAssets(previous: oldValue) } + } /// Read-only mirror of `panelAgentPresence.state`, kept for existing wire serialization and /// read call sites. Never write to this directly -- go through `updatePanelAgentState`. var panelAgentStates: [UUID: AgentActivityState] { diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 33c9eb56c..7ac9f1f5e 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -158,3 +158,30 @@ See the License for the specific language governing permissions and limitations under the License. + +--- + +## Lobe Icons + +- **License:** MIT License +- **Copyright:** Copyright (c) 2023 LobeHub +- **Source:** https://github.com/lobehub/lobe-icons +- **Used for:** the agent logos in `Assets.xcassets/AgentIcons`, taken from commit `a94750e3f5f8fc33757b839d85030e742284e43a` + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/programaTests/AgentActivityStateTests.swift b/programaTests/AgentActivityStateTests.swift index b803585cf..e09fef1be 100644 --- a/programaTests/AgentActivityStateTests.swift +++ b/programaTests/AgentActivityStateTests.swift @@ -635,3 +635,61 @@ final class AgentActivityStateTests: XCTestCase { XCTAssertEqual(workspace.panelAgentPresence[panel]?.isStale(now: Date()), false) } } + +@MainActor +final class TerminalTabAgentIconTests: XCTestCase { + func testResolverMapsEveryLogoAgentAndIgnoresCaseAndWhitespace() { + let expected: [String: String] = [ + "claude-code": "AgentIconClaudeCode", + "codex": "AgentIconCodex", + "copilot-cli": "AgentIconCopilot", + "cursor-agent": "AgentIconCursor", + "gemini-cli": "AgentIconGeminiCLI", + "opencode": "AgentIconOpenCode", + ] + for (identifier, asset) in expected { + XCTAssertEqual(TerminalTabAgentIconResolver.assetName(forAgent: identifier), asset, identifier) + } + XCTAssertEqual(TerminalTabAgentIconResolver.assetName(forAgent: " Claude-Code\n"), "AgentIconClaudeCode") + XCTAssertEqual(TerminalTabAgentIconResolver.assetName(forAgent: "CODEX"), "AgentIconCodex") + } + + func testResolverReturnsNilForAiderUnknownAndEmpty() { + XCTAssertNil(TerminalTabAgentIconResolver.assetName(forAgent: "aider")) + XCTAssertNil(TerminalTabAgentIconResolver.assetName(forAgent: "unknown")) + XCTAssertNil(TerminalTabAgentIconResolver.assetName(forAgent: "")) + } + + func testTabIconAssetFollowsHookPresenceAndClearsWhenAgentLeaves() throws { + let manager = TabManager() + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let panelId = try XCTUnwrap(workspace.focusedPanelId) + let tabId = try XCTUnwrap(workspace.surfaceIdFromPanelId(panelId)) + XCTAssertNil(workspace.bonsplitController.tab(tabId)?.iconAsset) + + workspace.updatePanelAgentState( + panelId: panelId, + state: .working, + source: .hooks, + sessionKey: AgentSessionKey(provider: "codex", sessionId: "s1", pid: nil) + ) + XCTAssertEqual(workspace.bonsplitController.tab(tabId)?.iconAsset, "AgentIconCodex") + + workspace.clearPanelAgentState(panelId: panelId) + XCTAssertNil(workspace.bonsplitController.tab(tabId)?.iconAsset) + } + + func testTabIconAssetFollowsInferredAgentAndSkipsAgentsWithoutLogo() throws { + let manager = TabManager() + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let panelId = try XCTUnwrap(workspace.focusedPanelId) + let tabId = try XCTUnwrap(workspace.surfaceIdFromPanelId(panelId)) + + workspace.updatePanelAgentState(panelId: panelId, state: .working, source: .inferred, inferredAgent: "gemini-cli") + XCTAssertEqual(workspace.bonsplitController.tab(tabId)?.iconAsset, "AgentIconGeminiCLI") + + workspace.clearPanelAgentState(panelId: panelId) + workspace.updatePanelAgentState(panelId: panelId, state: .working, source: .inferred, inferredAgent: "aider") + XCTAssertNil(workspace.bonsplitController.tab(tabId)?.iconAsset) + } +} diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Models/TabItem.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Models/TabItem.swift index 411cc4134..20fa5ba8d 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Models/TabItem.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Models/TabItem.swift @@ -20,6 +20,7 @@ struct TabItem: Identifiable, Hashable, Codable { var hasCustomTitle: Bool var icon: String? var iconImageData: Data? + var iconAsset: String? var kind: String? var isDirty: Bool var showsNotificationBadge: Bool @@ -33,6 +34,7 @@ struct TabItem: Identifiable, Hashable, Codable { hasCustomTitle: Bool = false, icon: String? = "doc.text", iconImageData: Data? = nil, + iconAsset: String? = nil, kind: String? = nil, isDirty: Bool = false, showsNotificationBadge: Bool = false, @@ -45,6 +47,7 @@ struct TabItem: Identifiable, Hashable, Codable { self.hasCustomTitle = hasCustomTitle self.icon = icon self.iconImageData = iconImageData + self.iconAsset = iconAsset self.kind = kind self.isDirty = isDirty self.showsNotificationBadge = showsNotificationBadge @@ -60,6 +63,7 @@ struct TabItem: Identifiable, Hashable, Codable { hasCustomTitle: tab.hasCustomTitle, icon: tab.icon, iconImageData: tab.iconImageData, + iconAsset: tab.iconAsset, kind: tab.kind, isDirty: tab.isDirty, showsNotificationBadge: tab.showsNotificationBadge, @@ -83,6 +87,7 @@ struct TabItem: Identifiable, Hashable, Codable { case hasCustomTitle case icon case iconImageData + case iconAsset case kind case isDirty case showsNotificationBadge @@ -98,6 +103,7 @@ struct TabItem: Identifiable, Hashable, Codable { self.hasCustomTitle = try c.decodeIfPresent(Bool.self, forKey: .hasCustomTitle) ?? false self.icon = try c.decodeIfPresent(String.self, forKey: .icon) self.iconImageData = try c.decodeIfPresent(Data.self, forKey: .iconImageData) + self.iconAsset = try c.decodeIfPresent(String.self, forKey: .iconAsset) self.kind = try c.decodeIfPresent(String.self, forKey: .kind) self.isDirty = try c.decodeIfPresent(Bool.self, forKey: .isDirty) ?? false self.showsNotificationBadge = try c.decodeIfPresent(Bool.self, forKey: .showsNotificationBadge) ?? false @@ -113,6 +119,7 @@ struct TabItem: Identifiable, Hashable, Codable { try c.encode(hasCustomTitle, forKey: .hasCustomTitle) try c.encodeIfPresent(icon, forKey: .icon) try c.encodeIfPresent(iconImageData, forKey: .iconImageData) + try c.encodeIfPresent(iconAsset, forKey: .iconAsset) try c.encodeIfPresent(kind, forKey: .kind) try c.encode(isDirty, forKey: .isDirty) try c.encode(showsNotificationBadge, forKey: .showsNotificationBadge) diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift index 19882748e..27fe911a2 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/PaneContainerView.swift @@ -284,6 +284,7 @@ private struct BonsplitPaneChromeAnchor: NSViewRepresentable { title: tab.title, icon: tab.icon, iconImageData: tab.iconImageData, + iconAsset: tab.iconAsset, isSelected: selectedTabID == tab.id, isPinned: tab.isPinned, isDirty: tab.isDirty, @@ -633,6 +634,7 @@ struct PaneContainerView<Content: View, EmptyContent: View>: View { hasher.combine(tab.hasCustomTitle) hasher.combine(tab.icon) hasher.combine(tab.iconImageData) + hasher.combine(tab.iconAsset) hasher.combine(tab.kind) hasher.combine(tab.isDirty) hasher.combine(tab.showsNotificationBadge) diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift index ab5838e1d..7330eea6b 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift @@ -104,6 +104,12 @@ struct TabItemView: View { FaviconIconView(image: image) .frame(width: iconSlotSize, height: iconSlotSize, alignment: .center) .clipped() + } else if let iconAsset = tab.iconAsset { + Image(iconAsset, bundle: .main) + .resizable() + .scaledToFit() + .frame(width: iconSlotSize, height: iconSlotSize, alignment: .center) + .clipped() } else if let iconName = tab.icon { if iconName == "globe", !showGlobeFallback { // Avoid a distracting "globe -> favicon" flash: show a neutral placeholder @@ -118,8 +124,8 @@ struct TabItemView: View { } } } - // Keep downloaded favicon bitmaps in full color even for inactive tab bars. - .saturation(TabItemStyling.iconSaturation(hasRasterIcon: faviconImage != nil, tabSaturation: saturation)) + // Keep downloaded favicon bitmaps and agent logos in full color even for inactive tab bars. + .saturation(TabItemStyling.iconSaturation(hasRasterIcon: faviconImage != nil || tab.iconAsset != nil, tabSaturation: saturation)) .transaction { tx in // Prevent incidental parent animations from briefly fading icon content. tx.animation = nil diff --git a/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitController.swift b/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitController.swift index 73e933df2..ac7ec1185 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitController.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitController.swift @@ -93,6 +93,7 @@ public final class BonsplitController { /// - title: The tab title /// - icon: Optional SF Symbol name for the tab icon /// - iconImageData: Optional image data (PNG recommended) for the tab icon. When present, takes precedence over `icon`. + /// - iconAsset: Optional asset-catalog image name (host main bundle). Takes precedence over `icon`, below `iconImageData`. /// - kind: Consumer-defined tab kind identifier (e.g. "terminal", "markdown") /// - hasCustomTitle: Whether the tab title came from a custom user override /// - isDirty: Whether the tab shows a dirty indicator @@ -107,6 +108,7 @@ public final class BonsplitController { hasCustomTitle: Bool = false, icon: String? = "doc.text", iconImageData: Data? = nil, + iconAsset: String? = nil, kind: String? = nil, isDirty: Bool = false, showsNotificationBadge: Bool = false, @@ -122,6 +124,7 @@ public final class BonsplitController { hasCustomTitle: hasCustomTitle, icon: icon, iconImageData: iconImageData, + iconAsset: iconAsset, kind: kind, isDirty: isDirty, showsNotificationBadge: showsNotificationBadge, @@ -187,6 +190,7 @@ public final class BonsplitController { /// - title: New title (pass nil to keep current) /// - icon: New icon (pass nil to keep current, pass .some(nil) to remove icon) /// - iconImageData: New icon image data (pass nil to keep current, pass .some(nil) to remove) + /// - iconAsset: New asset-catalog image name (pass nil to keep current, pass .some(nil) to remove) /// - kind: New tab kind (pass nil to keep current, pass .some(nil) to clear) /// - hasCustomTitle: New custom-title state (pass nil to keep current) /// - isDirty: New dirty state (pass nil to keep current) @@ -198,6 +202,7 @@ public final class BonsplitController { title: String? = nil, icon: String?? = nil, iconImageData: Data?? = nil, + iconAsset: String?? = nil, kind: String?? = nil, hasCustomTitle: Bool? = nil, isDirty: Bool? = nil, @@ -217,6 +222,9 @@ public final class BonsplitController { if let iconImageData = iconImageData { pane.tabs[tabIndex].iconImageData = iconImageData } + if let iconAsset = iconAsset { + pane.tabs[tabIndex].iconAsset = iconAsset + } if let kind = kind { pane.tabs[tabIndex].kind = kind } diff --git a/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitPaneChromePortalBridge.swift b/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitPaneChromePortalBridge.swift index 4d7eebce1..eb8d055b6 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitPaneChromePortalBridge.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitPaneChromePortalBridge.swift @@ -17,6 +17,7 @@ public struct BonsplitPaneChromeTabDescriptor { public let title: String public let icon: String? public let iconImageData: Data? + public let iconAsset: String? public let isSelected: Bool public let isPinned: Bool public let isDirty: Bool @@ -31,6 +32,7 @@ public struct BonsplitPaneChromeTabDescriptor { title: String, icon: String?, iconImageData: Data?, + iconAsset: String? = nil, isSelected: Bool, isPinned: Bool, isDirty: Bool, @@ -43,6 +45,7 @@ public struct BonsplitPaneChromeTabDescriptor { self.title = title self.icon = icon self.iconImageData = iconImageData + self.iconAsset = iconAsset self.isSelected = isSelected self.isPinned = isPinned self.isDirty = isDirty diff --git a/vendor/bonsplit/Sources/Bonsplit/Public/Types/Tab.swift b/vendor/bonsplit/Sources/Bonsplit/Public/Types/Tab.swift index 2c412d03a..6bb416adf 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Public/Types/Tab.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Public/Types/Tab.swift @@ -8,6 +8,8 @@ public struct Tab: Identifiable, Hashable, Sendable { public let icon: String? /// Optional image data (PNG recommended) for the tab icon. When present, this takes precedence over `icon`. public let iconImageData: Data? + /// Optional asset-catalog image name, resolved in the host app's main bundle. Takes precedence over `icon` but not over `iconImageData`. + public let iconAsset: String? /// Consumer-defined tab kind identifier (for example, "terminal" or "markdown"). public let kind: String? public let isDirty: Bool @@ -26,6 +28,7 @@ public struct Tab: Identifiable, Hashable, Sendable { hasCustomTitle: Bool = false, icon: String? = nil, iconImageData: Data? = nil, + iconAsset: String? = nil, kind: String? = nil, isDirty: Bool = false, showsNotificationBadge: Bool = false, @@ -38,6 +41,7 @@ public struct Tab: Identifiable, Hashable, Sendable { self.hasCustomTitle = hasCustomTitle self.icon = icon self.iconImageData = iconImageData + self.iconAsset = iconAsset self.kind = kind self.isDirty = isDirty self.showsNotificationBadge = showsNotificationBadge @@ -52,6 +56,7 @@ public struct Tab: Identifiable, Hashable, Sendable { self.hasCustomTitle = tabItem.hasCustomTitle self.icon = tabItem.icon self.iconImageData = tabItem.iconImageData + self.iconAsset = tabItem.iconAsset self.kind = tabItem.kind self.isDirty = tabItem.isDirty self.showsNotificationBadge = tabItem.showsNotificationBadge