Skip to content

feat(sidebar): show a project's own icon from project-icon.svg - #67

Merged
ghackett merged 5 commits into
mainfrom
feat/project-icons
Jul 29, 2026
Merged

feat(sidebar): show a project's own icon from project-icon.svg#67
ghackett merged 5 commits into
mainfrom
feat/project-icons

Conversation

@ghackett

@ghackett ghackett commented Jul 29, 2026

Copy link
Copy Markdown
Member

What

Each project can now ship its own sidebar icon: drop a project-icon.svg in the project's root directory and Collins shows it in place of the generic folder icon next to the project name. Projects without one keep the folder-symbolic fallback, and Favorites/Chats keep their existing icons. The header row's arrow, icon, and label are now explicitly vertically centered so the text sits properly against whichever icon renders.

This repo now ships its own project-icon.svg (the Tom Collins glass app icon), so Collins-on-Collins dogfoods the feature.

How

  • New GTK-free collins/projecticons.py (modeled on gitinfo.py) resolves <cwd>/project-icon.svg, ignoring missing, empty, or implausibly large (>256 KB) files — headlessly unit-testable.
  • GroupHeaderRow rasterizes the custom icon through a GdkPixbuf.PixbufLoader forced to the SVG codec at exactly the icon size. Custom icons skip the dim-label class so they keep their own colors.

Security

A project row can exist before any agent has run in its directory (new session from within the app), so project-icon.svg may be the first repo-controlled content Collins touches. Hardening for untrusted repos:

  • projecticons vets the bytes, not just the path: gzip/svgz is refused (it would decompress past the size cap inside librsvg), and content must be XML-shaped text with an <svg> element near the top — crafted binaries for other codecs never leave the module.
  • The forced-type loader means gdk-pixbuf content sniffing can never route repo bytes to another installed decoder (TIFF/ICO/etc.), and decoding at icon size bounds the raster surface regardless of the document's own canvas dimensions.
  • librsvg itself executes no scripts and refuses network URLs, so the web-style SVG attack classes don't apply.
  • Remaining (theoretical) exposure — an expensive-but-valid SVG hanging the synchronous main-thread decode — is tracked in Pathological project-icon.svg can freeze the UI (synchronous decode on the main thread) #68.
  • Icons are picked up at row build time; the sidebar's refresh button forces a full row rebuild (store.refresh(force_rebuild=True)), so adding or changing an icon while Collins runs shows up on refresh — no file watching.

Screenshots

alpha-widgets ships a project-icon.svg (blue bolt); beta-server has none and falls back to the folder icon.

Before After
Sidebar before: folder icons everywhere Sidebar after: alpha-widgets shows its custom bolt icon

Full window:

Before After
Full window before Full window after

Icon size setting

A new Project icon size spin row (16–32 px, default 16) under the Session list preferences sizes every group-header icon — custom project icons, the folder fallback, and the Favorites/Chats icons — consistently. Existing rows resize in place when the setting changes. At 24 px:

Sidebar detail (24 px) Full window (24 px)
Sidebar with 24px project icons Full window with 24px project icons

Testing

  • python3 -m pytest tests/ -q — 269 passed (14 new in tests/test_projecticons.py, 1 new in tests/test_store.py)
  • ruff check collins/ tests/ — clean
  • Screenshots above captured from an isolated e2e instance with staged data

🤖 Generated with Claude Code

https://claude.ai/code/session_01NWcqNwkCLdwN4gExvhAAwC

ghackett and others added 5 commits July 29, 2026 06:41
A project can now ship a project-icon.svg in its root directory; the
sidebar shows it in place of the generic folder icon, at the same 16px
size. Discovery lives in a GTK-free projecticons module (empty or
oversized files are ignored) so it is unit-testable headless. The
header row's arrow, icon, and label are now explicitly valign-centered
so the text sits properly against whichever icon renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWcqNwkCLdwN4gExvhAAwC
The sidebar only rebuilds rows when the store reports an order change,
so a manual refresh with an unchanged session list never re-read
project-icon.svg. The refresh action now passes force_rebuild=True,
a one-shot flag that makes the post-scan refreshed signal report an
order change even when nothing moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWcqNwkCLdwN4gExvhAAwC
Dogfoods the new sidebar feature: Collins working on Collins shows the
Tom Collins glass instead of the generic folder icon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWcqNwkCLdwN4gExvhAAwC
New "Project icon size" spin row (16-32 px, default 16) under the
Session list preferences. Applies to every group-header icon — custom
project icons, the folder fallback, and the Favorites/Chats icons —
so the sidebar stays visually consistent. Existing header rows resize
in place when the setting changes; new rows read it at build time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWcqNwkCLdwN4gExvhAAwC
A project row can exist before any agent has run in its directory (new
session from within the app), so project-icon.svg may be the first
repo-controlled content Collins touches. Two layers of hardening:

- projecticons now vets the bytes, not just the path: gzip/svgz is
  refused (it would expand past the size cap inside librsvg) and the
  content must be XML-shaped text with an <svg> element near the top,
  so crafted binaries for other image codecs never leave the module.
- The sidebar rasterizes through a PixbufLoader forced to the "svg"
  type — gdk-pixbuf content sniffing can no longer route repo bytes to
  another installed codec — and decodes at icon size, so the raster
  surface is bounded regardless of the document's canvas dimensions.
  Icon-size changes re-render the texture so custom icons stay sharp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWcqNwkCLdwN4gExvhAAwC
@ghackett
ghackett marked this pull request as ready for review July 29, 2026 11:16
@ghackett
ghackett merged commit 333ce0c into main Jul 29, 2026
2 checks passed
@ghackett
ghackett deleted the feat/project-icons branch July 29, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant