${escapeHtml(a.name)}
${escapeHtml(a.description) || 'no description'}
@@ -5861,7 +5882,7 @@ function renderActivities(data) {
const isAttached = attachedIds.has(d.id);
return `
- ${d.icon || '๐'}
+ ${d.icon_svg || escapeHtml(d.emoji || '')}
${escapeHtml(d.name)}
`;
diff --git a/solstone/think/activities.py b/solstone/think/activities.py
index 66edfa55f..c12c59608 100644
--- a/solstone/think/activities.py
+++ b/solstone/think/activities.py
@@ -27,6 +27,7 @@ from solstone.think.utils import get_journal, segment_parse
logger = logging.getLogger(__name__)
ANTICIPATION_FUZZY_THRESHOLD = 0.85
+LUCIDE_ICON_NAME_RE = re.compile(r"^[a-z0-9-]+$")
# ---------------------------------------------------------------------------
# Default Activities
@@ -40,7 +41,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "meeting",
"name": "Meetings",
"description": "Video calls, in-person meetings, and conferences",
- "icon": "๐
",
+ "emoji": "๐
",
+ "icon": "users",
"always_on": True,
"instructions": (
"Levels: high=actively speaking/presenting, medium=listening attentively,"
@@ -52,70 +54,80 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "call",
"name": "call",
"description": "a call you have planned",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "phone",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "deadline",
"name": "deadline",
"description": "a deadline you are working toward",
- "icon": "โฐ",
+ "emoji": "โฐ",
+ "icon": "alarm-clock",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "appointment",
"name": "appointment",
"description": "an appointment on your calendar",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "pin",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "event",
"name": "event",
"description": "an event you plan to attend",
- "icon": "๐๏ธ",
+ "emoji": "๐๏ธ",
+ "icon": "ticket",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "travel",
"name": "travel",
"description": "travel you have planned",
- "icon": "โ๏ธ",
+ "emoji": "โ๏ธ",
+ "icon": "plane",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "reminder",
"name": "reminder",
"description": "a reminder for something upcoming",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "bell",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "errand",
"name": "errand",
"description": "an errand you plan to do",
- "icon": "๐งพ",
+ "emoji": "๐งพ",
+ "icon": "receipt",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "celebration",
"name": "celebration",
"description": "a celebration on the calendar",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "party-popper",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "doctor_appointment",
"name": "doctor appointment",
"description": "a medical appointment on your calendar",
- "icon": "๐ฉบ",
+ "emoji": "๐ฉบ",
+ "icon": "stethoscope",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
},
{
"id": "coding",
"name": "Coding",
"description": "Programming, code review, and debugging",
- "icon": "๐ป",
+ "emoji": "๐ป",
+ "icon": "code-xml",
"instructions": (
"Levels: high=writing or debugging code, medium=reading/reviewing code,"
" low=IDE or editor open but not focused."
@@ -127,7 +139,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "browsing",
"name": "Browsing",
"description": "Web browsing, research, and reading online",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "globe",
"instructions": (
"Levels: high=actively navigating/researching, medium=reading a page,"
" low=browser open but idle."
@@ -138,7 +151,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "email",
"name": "Email",
"description": "Email reading and composition",
- "icon": "๐ง",
+ "emoji": "๐ง",
+ "icon": "mail",
"always_on": True,
"instructions": (
"Levels: high=composing or actively reading email,"
@@ -150,7 +164,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "messaging",
"name": "Messaging",
"description": "Chat, Slack, Discord, and text messaging",
- "icon": "๐ฌ",
+ "emoji": "๐ฌ",
+ "icon": "messages-square",
"always_on": True,
"instructions": (
"Levels: high=active conversation, medium=reading messages,"
@@ -162,7 +177,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "ai_conversation",
"name": "AI Conversation",
"description": "Conversations with AI assistants like ChatGPT, Claude, and Gemini",
- "icon": "๐ค",
+ "emoji": "๐ค",
+ "icon": "bot",
"instructions": (
"Levels: high=actively prompting and reading responses,"
" medium=reviewing AI output or refining prompts,"
@@ -177,7 +193,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "writing",
"name": "Writing",
"description": "Documents, notes, and long-form writing",
- "icon": "โ๏ธ",
+ "emoji": "โ๏ธ",
+ "icon": "pencil-line",
"instructions": (
"Levels: high=actively composing text, medium=editing/revising,"
" low=document open but not being edited."
@@ -188,7 +205,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "reading",
"name": "Reading",
"description": "Books, PDFs, articles, highlights, and documentation",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "book-open",
"instructions": (
"Levels: high=focused reading, medium=skimming content,"
" low=document open but attention elsewhere."
@@ -201,7 +219,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "video",
"name": "Video",
"description": "Watching videos and streaming content",
- "icon": "๐ฌ",
+ "emoji": "๐ฌ",
+ "icon": "clapperboard",
"instructions": (
"Levels: high=actively watching, medium=video playing while"
" doing something else, low=video paused or minimized."
@@ -212,7 +231,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "gaming",
"name": "Gaming",
"description": "Games and entertainment",
- "icon": "๐ฎ",
+ "emoji": "๐ฎ",
+ "icon": "gamepad-2",
"instructions": (
"Levels: high=actively playing, medium=in menus or waiting,"
" low=game open but tabbed out."
@@ -223,7 +243,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "social",
"name": "Social Media",
"description": "Social media browsing and interaction",
- "icon": "๐ฑ",
+ "emoji": "๐ฑ",
+ "icon": "share-2",
"instructions": (
"Levels: high=posting or actively engaging, medium=scrolling feed,"
" low=social app open but idle."
@@ -234,7 +255,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "planning",
"name": "Planning",
"description": "Scheduling, calendar management, meeting preparation, and agenda setting",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "calendar-check",
"instructions": (
"Levels: high=actively scheduling or preparing agendas,"
" medium=reviewing calendar or event details,"
@@ -250,7 +272,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "productivity",
"name": "Productivity",
"description": "Spreadsheets, slides, and task management",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "chart-column",
"instructions": (
"Levels: high=actively editing or organizing, medium=reviewing data,"
" low=app open but not focused."
@@ -262,7 +285,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "terminal",
"name": "Terminal",
"description": "Command line and shell sessions",
- "icon": "โจ๏ธ",
+ "emoji": "โจ๏ธ",
+ "icon": "terminal",
"instructions": (
"Levels: high=running commands or scripts, medium=reading output,"
" low=terminal open but idle."
@@ -274,7 +298,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "design",
"name": "Design",
"description": "Design tools and image editing",
- "icon": "๐จ",
+ "emoji": "๐จ",
+ "icon": "palette",
"instructions": (
"Levels: high=actively creating or editing, medium=reviewing designs,"
" low=design tool open but idle."
@@ -285,7 +310,8 @@ DEFAULT_ACTIVITIES: list[dict[str, str]] = [
"id": "music",
"name": "Music",
"description": "Music listening and audio",
- "icon": "๐ต",
+ "emoji": "๐ต",
+ "icon": "music",
"instructions": (
"Levels: high=actively choosing or browsing music,"
" medium=playlist running while working, low=ambient background audio."
@@ -304,6 +330,27 @@ def get_default_activities() -> list[dict[str, str]]:
return [dict(a) for a in DEFAULT_ACTIVITIES]
+def _lucide_name_shaped(value: object) -> bool:
+ return isinstance(value, str) and bool(LUCIDE_ICON_NAME_RE.fullmatch(value))
+
+
+def _normalized_icon_fields(entry: dict[str, Any]) -> tuple[str | None, str | None]:
+ """Return canonical (emoji, icon) fields for a stored activity config entry."""
+ emoji = entry.get("emoji")
+ icon = entry.get("icon")
+
+ if not isinstance(emoji, str) or not emoji:
+ emoji = None
+
+ if not isinstance(icon, str) or not icon:
+ return emoji, None
+
+ if _lucide_name_shaped(icon):
+ return emoji, icon
+
+ return emoji or icon, None
+
+
def _get_activities_path(facet: str) -> Path:
"""Get the path to a facet's activities.jsonl file."""
return Path(get_journal()) / "facets" / facet / "activities" / "activities.jsonl"
@@ -358,7 +405,8 @@ def get_facet_activities(facet: str) -> list[dict[str, Any]]:
- id: Activity identifier
- name: Display name
- description: Activity description
- - icon: Emoji icon (if predefined)
+ - emoji: Glyph fallback
+ - icon: Lucide icon name
- priority: "high", "normal", or "low"
- custom: True if user-created (not in defaults)
- always_on: True if auto-included from defaults
@@ -395,6 +443,8 @@ def get_facet_activities(facet: str) -> list[dict[str, Any]]:
else:
activity = {"id": activity_id, "custom": True}
+ emoji, icon = _normalized_icon_fields(fa)
+
# Apply facet overrides
if "name" in fa:
activity["name"] = fa["name"]
@@ -402,8 +452,10 @@ def get_facet_activities(facet: str) -> list[dict[str, Any]]:
activity["description"] = fa["description"]
if "priority" in fa:
activity["priority"] = fa["priority"]
- if "icon" in fa:
- activity["icon"] = fa["icon"]
+ if emoji is not None:
+ activity["emoji"] = emoji
+ if icon is not None:
+ activity["icon"] = icon
if "instructions" in fa:
activity["instructions"] = fa["instructions"]
@@ -437,7 +489,8 @@ def save_facet_activities(facet: str, activities: list[dict[str, Any]]) -> None:
- description: Activity description
Optional for all:
- priority: "high", "normal", or "low"
- - icon: Emoji icon
+ - emoji: Glyph fallback
+ - icon: Lucide icon name
- instructions: Detection/level instructions for the LLM
"""
# Build lookup for defaults to determine what needs to be stored
@@ -482,6 +535,8 @@ def save_facet_activities(facet: str, activities: list[dict[str, Any]]) -> None:
entry["instructions"] = activity["instructions"]
if activity.get("priority"):
entry["priority"] = activity["priority"]
+ if activity.get("emoji"):
+ entry["emoji"] = activity["emoji"]
if activity.get("icon"):
entry["icon"] = activity["icon"]
@@ -490,6 +545,64 @@ def save_facet_activities(facet: str, activities: list[dict[str, Any]]) -> None:
_save_activities_jsonl(facet, entries)
+def migrate_custom_activity_icons_to_emoji(
+ *, dry_run: bool = False
+) -> dict[str, int | bool]:
+ """Migrate legacy custom activity glyphs from icon to emoji.
+
+ Stored custom activities used to treat ``icon`` as an emoji glyph. The
+ current shape reserves ``icon`` for Lucide names and stores glyphs in
+ ``emoji``. Predefined activities are intentionally left alone because
+ public write paths never stored icon overrides for them.
+ """
+ defaults_by_id = {a["id"]: a for a in DEFAULT_ACTIVITIES}
+ facets_dir = Path(get_journal()) / "facets"
+ result: dict[str, int | bool] = {
+ "dry_run": dry_run,
+ "files_scanned": 0,
+ "files_changed": 0,
+ "records_changed": 0,
+ }
+ if not facets_dir.exists():
+ return result
+
+ for path in sorted(facets_dir.glob("*/activities/activities.jsonl")):
+ facet = path.parent.parent.name
+ records = _load_activities_jsonl(facet)
+ result["files_scanned"] = int(result["files_scanned"]) + 1
+ changed = False
+
+ for record in records:
+ activity_id = record.get("id")
+ if not activity_id:
+ continue
+ if not (record.get("custom") or activity_id not in defaults_by_id):
+ continue
+
+ record_changed = False
+ emoji, icon = _normalized_icon_fields(record)
+ if emoji is not None and record.get("emoji") != emoji:
+ record["emoji"] = emoji
+ record_changed = True
+ if icon is None and "icon" in record:
+ record.pop("icon", None)
+ record_changed = True
+ elif icon is not None and record.get("icon") != icon:
+ record["icon"] = icon
+ record_changed = True
+
+ if record_changed:
+ changed = True
+ result["records_changed"] = int(result["records_changed"]) + 1
+
+ if changed:
+ result["files_changed"] = int(result["files_changed"]) + 1
+ if not dry_run:
+ _save_activities_jsonl(facet, records)
+
+ return result
+
+
def get_default_activity_by_id(activity_id: str) -> dict[str, Any] | None:
"""Look up a predefined default activity by ID."""
for activity in DEFAULT_ACTIVITIES:
@@ -539,6 +652,7 @@ def add_activity_to_facet(
description: str | None = None,
instructions: str | None = None,
priority: str = "normal",
+ emoji: str | None = None,
icon: str | None = None,
) -> dict[str, Any]:
"""Add an activity to a facet.
@@ -553,7 +667,8 @@ def add_activity_to_facet(
description: Activity description
instructions: Detection/level instructions for the LLM
priority: "high", "normal", or "low"
- icon: Emoji icon
+ emoji: Glyph fallback
+ icon: Lucide icon name
Returns:
The added activity dict
@@ -589,6 +704,8 @@ def add_activity_to_facet(
activity["instructions"] = instructions
if priority and priority != "normal":
activity["priority"] = priority
+ if emoji:
+ activity["emoji"] = emoji
if icon:
activity["icon"] = icon
@@ -630,6 +747,7 @@ def update_activity_in_facet(
instructions: str | None = None,
priority: str | None = None,
name: str | None = None,
+ emoji: str | None = None,
icon: str | None = None,
) -> dict[str, Any] | None:
"""Update an activity's configuration in a facet.
@@ -641,7 +759,8 @@ def update_activity_in_facet(
instructions: New detection/level instructions (None to keep existing)
priority: New priority (None to keep existing)
name: New name - only applies to custom activities
- icon: New icon - only applies to custom activities
+ emoji: New glyph fallback - only applies to custom activities
+ icon: New Lucide icon name - only applies to custom activities
Returns:
Updated activity dict, or None if not found
@@ -670,12 +789,20 @@ def update_activity_in_facet(
else:
activity["priority"] = priority
- # Only allow name/icon changes for custom activities
+ # Only allow name/emoji/icon changes for custom activities
if activity.get("custom") or activity_id not in defaults_by_id:
if name is not None:
activity["name"] = name
+ if emoji is not None:
+ if emoji:
+ activity["emoji"] = emoji
+ else:
+ activity.pop("emoji", None)
if icon is not None:
- activity["icon"] = icon
+ if icon:
+ activity["icon"] = icon
+ else:
+ activity.pop("icon", None)
break
diff --git a/tests/baselines/api/settings/activities-defaults.json b/tests/baselines/api/settings/activities-defaults.json
index e4c4ee015..9ad23a67f 100644
--- a/tests/baselines/api/settings/activities-defaults.json
+++ b/tests/baselines/api/settings/activities-defaults.json
@@ -3,7 +3,9 @@
{
"always_on": true,
"description": "Chat, Slack, Discord, and text messaging",
- "icon": "๐ฌ",
+ "emoji": "๐ฌ",
+ "icon": "messages-square",
+ "icon_svg": "
",
"id": "messaging",
"instructions": "Levels: high=active conversation, medium=reading messages, low=chat app visible but idle. Detect via: chat app UI, message notifications, typing indicators.",
"name": "Messaging"
@@ -11,7 +13,9 @@
{
"always_on": true,
"description": "Email reading and composition",
- "icon": "๐ง",
+ "emoji": "๐ง",
+ "icon": "mail",
+ "icon_svg": "
",
"id": "email",
"instructions": "Levels: high=composing or actively reading email, medium=scanning inbox, low=email client visible but idle. Detect via: email client UI, inbox view, compose window.",
"name": "Email"
@@ -19,161 +23,207 @@
{
"always_on": true,
"description": "Video calls, in-person meetings, and conferences",
- "icon": "๐
",
+ "emoji": "๐
",
+ "icon": "users",
+ "icon_svg": "
",
"id": "meeting",
"instructions": "Levels: high=actively speaking/presenting, medium=listening attentively, low=muted or multitasking during call. Detect via: video call UI, multiple speakers, calendar event visible.",
"name": "Meetings"
},
{
"description": "Books, PDFs, articles, highlights, and documentation",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "book-open",
+ "icon_svg": "
",
"id": "reading",
"instructions": "Levels: high=focused reading, medium=skimming content, low=document open but attention elsewhere. Detect via: PDF viewers, article pages, documentation sites, reading apps, imported book highlights and annotations. Do not use for reading code โ that is coding.",
"name": "Reading"
},
{
"description": "Command line and shell sessions",
- "icon": "โจ๏ธ",
+ "emoji": "โจ๏ธ",
+ "icon": "terminal",
+ "icon_svg": "
",
"id": "terminal",
"instructions": "Levels: high=running commands or scripts, medium=reading output, low=terminal open but idle. Detect via: shell prompts, command output, tmux/screen sessions. If terminal use is clearly coding-related, prefer coding instead.",
"name": "Terminal"
},
{
"description": "Conversations with AI assistants like ChatGPT, Claude, and Gemini",
- "icon": "๐ค",
+ "emoji": "๐ค",
+ "icon": "bot",
+ "icon_svg": "
",
"id": "ai_conversation",
"instructions": "Levels: high=actively prompting and reading responses, medium=reviewing AI output or refining prompts, low=AI chat open but idle. Detect via: AI assistant interfaces (ChatGPT, Claude, Gemini), imported AI conversation transcripts, prompt-response patterns. Do not confuse with messaging โ AI conversation involves a human interacting with an AI model, not person-to-person chat.",
"name": "AI Conversation"
},
{
"description": "Design tools and image editing",
- "icon": "๐จ",
+ "emoji": "๐จ",
+ "icon": "palette",
+ "icon_svg": "
",
"id": "design",
"instructions": "Levels: high=actively creating or editing, medium=reviewing designs, low=design tool open but idle. Detect via: design apps (Figma, Photoshop, etc), canvas editing.",
"name": "Design"
},
{
"description": "Documents, notes, and long-form writing",
- "icon": "โ๏ธ",
+ "emoji": "โ๏ธ",
+ "icon": "pencil-line",
+ "icon_svg": "
",
"id": "writing",
"instructions": "Levels: high=actively composing text, medium=editing/revising, low=document open but not being edited. Detect via: document editors, note apps, text content changing.",
"name": "Writing"
},
{
"description": "Games and entertainment",
- "icon": "๐ฎ",
+ "emoji": "๐ฎ",
+ "icon": "gamepad-2",
+ "icon_svg": "
",
"id": "gaming",
"instructions": "Levels: high=actively playing, medium=in menus or waiting, low=game open but tabbed out. Detect via: game window, controller input, game UI elements.",
"name": "Gaming"
},
{
"description": "Music listening and audio",
- "icon": "๐ต",
+ "emoji": "๐ต",
+ "icon": "music",
+ "icon_svg": "
",
"id": "music",
"instructions": "Levels: high=actively choosing or browsing music, medium=playlist running while working, low=ambient background audio. Detect via: music player UI, audio playback indicators.",
"name": "Music"
},
{
"description": "Programming, code review, and debugging",
- "icon": "๐ป",
+ "emoji": "๐ป",
+ "icon": "code-xml",
+ "icon_svg": "
",
"id": "coding",
"instructions": "Levels: high=writing or debugging code, medium=reading/reviewing code, low=IDE or editor open but not focused. Detect via: editors, terminals with dev tools, AI coding assistants, git operations. Includes focused code reading and thinking.",
"name": "Coding"
},
{
"description": "Scheduling, calendar management, meeting preparation, and agenda setting",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "calendar-check",
+ "icon_svg": "
",
"id": "planning",
"instructions": "Levels: high=actively scheduling or preparing agendas, medium=reviewing calendar or event details, low=calendar visible but not being interacted with. Detect via: calendar apps, scheduling interfaces, event creation, imported calendar events, meeting invitations, agenda drafting. Use for scheduling and preparation work. Do not confuse with meeting โ planning is the preparation, meeting is the actual synchronous interaction.",
"name": "Planning"
},
{
"description": "Social media browsing and interaction",
- "icon": "๐ฑ",
+ "emoji": "๐ฑ",
+ "icon": "share-2",
+ "icon_svg": "
",
"id": "social",
"instructions": "Levels: high=posting or actively engaging, medium=scrolling feed, low=social app open but idle. Detect via: social media sites/apps, feed content, post composition.",
"name": "Social Media"
},
{
"description": "Spreadsheets, slides, and task management",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "chart-column",
+ "icon_svg": "
",
"id": "productivity",
"instructions": "Levels: high=actively editing or organizing, medium=reviewing data, low=app open but not focused. Detect via: spreadsheet/slide editors, project management tools, task boards.",
"name": "Productivity"
},
{
"description": "Watching videos and streaming content",
- "icon": "๐ฌ",
+ "emoji": "๐ฌ",
+ "icon": "clapperboard",
+ "icon_svg": "
",
"id": "video",
"instructions": "Levels: high=actively watching, medium=video playing while doing something else, low=video paused or minimized. Detect via: video player UI, streaming sites, playback controls.",
"name": "Video"
},
{
"description": "Web browsing, research, and reading online",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "globe",
+ "icon_svg": "
",
"id": "browsing",
"instructions": "Levels: high=actively navigating/researching, medium=reading a page, low=browser open but idle. Detect via: browser tabs, URL changes, search queries.",
"name": "Browsing"
},
{
"description": "a call you have planned",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "phone",
+ "icon_svg": "
",
"id": "call",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "call"
},
{
"description": "a celebration on the calendar",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "party-popper",
+ "icon_svg": "
",
"id": "celebration",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "celebration"
},
{
"description": "a deadline you are working toward",
- "icon": "โฐ",
+ "emoji": "โฐ",
+ "icon": "alarm-clock",
+ "icon_svg": "
",
"id": "deadline",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "deadline"
},
{
"description": "a medical appointment on your calendar",
- "icon": "๐ฉบ",
+ "emoji": "๐ฉบ",
+ "icon": "stethoscope",
+ "icon_svg": "
",
"id": "doctor_appointment",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "doctor appointment"
},
{
"description": "a reminder for something upcoming",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "bell",
+ "icon_svg": "
",
"id": "reminder",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "reminder"
},
{
"description": "an appointment on your calendar",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "pin",
+ "icon_svg": "
",
"id": "appointment",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "appointment"
},
{
"description": "an errand you plan to do",
- "icon": "๐งพ",
+ "emoji": "๐งพ",
+ "icon": "receipt",
+ "icon_svg": "
",
"id": "errand",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "errand"
},
{
"description": "an event you plan to attend",
- "icon": "๐๏ธ",
+ "emoji": "๐๏ธ",
+ "icon": "ticket",
+ "icon_svg": "
",
"id": "event",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "event"
},
{
"description": "travel you have planned",
- "icon": "โ๏ธ",
+ "emoji": "โ๏ธ",
+ "icon": "plane",
+ "icon_svg": "
",
"id": "travel",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "travel"
diff --git a/tests/baselines/api/settings/facet-activities.json b/tests/baselines/api/settings/facet-activities.json
index 6961b74d4..857365b6e 100644
--- a/tests/baselines/api/settings/facet-activities.json
+++ b/tests/baselines/api/settings/facet-activities.json
@@ -4,7 +4,9 @@
"always_on": true,
"custom": false,
"description": "Chat, Slack, Discord, and text messaging",
- "icon": "๐ฌ",
+ "emoji": "๐ฌ",
+ "icon": "messages-square",
+ "icon_svg": "
",
"id": "messaging",
"instructions": "Levels: high=active conversation, medium=reading messages, low=chat app visible but idle. Detect via: chat app UI, message notifications, typing indicators.",
"name": "Messaging",
@@ -14,7 +16,9 @@
"always_on": true,
"custom": false,
"description": "Email reading and composition",
- "icon": "๐ง",
+ "emoji": "๐ง",
+ "icon": "mail",
+ "icon_svg": "
",
"id": "email",
"instructions": "Levels: high=composing or actively reading email, medium=scanning inbox, low=email client visible but idle. Detect via: email client UI, inbox view, compose window.",
"name": "Email",
@@ -24,7 +28,9 @@
"always_on": true,
"custom": false,
"description": "Meetings and sync calls",
- "icon": "๐
",
+ "emoji": "๐
",
+ "icon": "users",
+ "icon_svg": "
",
"id": "meeting",
"instructions": "Levels: high=actively speaking/presenting, medium=listening attentively, low=muted or multitasking during call. Detect via: video call UI, multiple speakers, calendar event visible.",
"name": "Meetings",
@@ -33,6 +39,7 @@
{
"custom": true,
"description": "Software development and coding",
+ "icon_svg": null,
"id": "engineering",
"name": "Engineering",
"priority": "high"
@@ -42,7 +49,9 @@
{
"always_on": true,
"description": "Chat, Slack, Discord, and text messaging",
- "icon": "๐ฌ",
+ "emoji": "๐ฌ",
+ "icon": "messages-square",
+ "icon_svg": "
",
"id": "messaging",
"instructions": "Levels: high=active conversation, medium=reading messages, low=chat app visible but idle. Detect via: chat app UI, message notifications, typing indicators.",
"name": "Messaging"
@@ -50,7 +59,9 @@
{
"always_on": true,
"description": "Email reading and composition",
- "icon": "๐ง",
+ "emoji": "๐ง",
+ "icon": "mail",
+ "icon_svg": "
",
"id": "email",
"instructions": "Levels: high=composing or actively reading email, medium=scanning inbox, low=email client visible but idle. Detect via: email client UI, inbox view, compose window.",
"name": "Email"
@@ -58,161 +69,207 @@
{
"always_on": true,
"description": "Video calls, in-person meetings, and conferences",
- "icon": "๐
",
+ "emoji": "๐
",
+ "icon": "users",
+ "icon_svg": "
",
"id": "meeting",
"instructions": "Levels: high=actively speaking/presenting, medium=listening attentively, low=muted or multitasking during call. Detect via: video call UI, multiple speakers, calendar event visible.",
"name": "Meetings"
},
{
"description": "Books, PDFs, articles, highlights, and documentation",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "book-open",
+ "icon_svg": "
",
"id": "reading",
"instructions": "Levels: high=focused reading, medium=skimming content, low=document open but attention elsewhere. Detect via: PDF viewers, article pages, documentation sites, reading apps, imported book highlights and annotations. Do not use for reading code โ that is coding.",
"name": "Reading"
},
{
"description": "Command line and shell sessions",
- "icon": "โจ๏ธ",
+ "emoji": "โจ๏ธ",
+ "icon": "terminal",
+ "icon_svg": "
",
"id": "terminal",
"instructions": "Levels: high=running commands or scripts, medium=reading output, low=terminal open but idle. Detect via: shell prompts, command output, tmux/screen sessions. If terminal use is clearly coding-related, prefer coding instead.",
"name": "Terminal"
},
{
"description": "Conversations with AI assistants like ChatGPT, Claude, and Gemini",
- "icon": "๐ค",
+ "emoji": "๐ค",
+ "icon": "bot",
+ "icon_svg": "
",
"id": "ai_conversation",
"instructions": "Levels: high=actively prompting and reading responses, medium=reviewing AI output or refining prompts, low=AI chat open but idle. Detect via: AI assistant interfaces (ChatGPT, Claude, Gemini), imported AI conversation transcripts, prompt-response patterns. Do not confuse with messaging โ AI conversation involves a human interacting with an AI model, not person-to-person chat.",
"name": "AI Conversation"
},
{
"description": "Design tools and image editing",
- "icon": "๐จ",
+ "emoji": "๐จ",
+ "icon": "palette",
+ "icon_svg": "
",
"id": "design",
"instructions": "Levels: high=actively creating or editing, medium=reviewing designs, low=design tool open but idle. Detect via: design apps (Figma, Photoshop, etc), canvas editing.",
"name": "Design"
},
{
"description": "Documents, notes, and long-form writing",
- "icon": "โ๏ธ",
+ "emoji": "โ๏ธ",
+ "icon": "pencil-line",
+ "icon_svg": "
",
"id": "writing",
"instructions": "Levels: high=actively composing text, medium=editing/revising, low=document open but not being edited. Detect via: document editors, note apps, text content changing.",
"name": "Writing"
},
{
"description": "Games and entertainment",
- "icon": "๐ฎ",
+ "emoji": "๐ฎ",
+ "icon": "gamepad-2",
+ "icon_svg": "
",
"id": "gaming",
"instructions": "Levels: high=actively playing, medium=in menus or waiting, low=game open but tabbed out. Detect via: game window, controller input, game UI elements.",
"name": "Gaming"
},
{
"description": "Music listening and audio",
- "icon": "๐ต",
+ "emoji": "๐ต",
+ "icon": "music",
+ "icon_svg": "
",
"id": "music",
"instructions": "Levels: high=actively choosing or browsing music, medium=playlist running while working, low=ambient background audio. Detect via: music player UI, audio playback indicators.",
"name": "Music"
},
{
"description": "Programming, code review, and debugging",
- "icon": "๐ป",
+ "emoji": "๐ป",
+ "icon": "code-xml",
+ "icon_svg": "
",
"id": "coding",
"instructions": "Levels: high=writing or debugging code, medium=reading/reviewing code, low=IDE or editor open but not focused. Detect via: editors, terminals with dev tools, AI coding assistants, git operations. Includes focused code reading and thinking.",
"name": "Coding"
},
{
"description": "Scheduling, calendar management, meeting preparation, and agenda setting",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "calendar-check",
+ "icon_svg": "
",
"id": "planning",
"instructions": "Levels: high=actively scheduling or preparing agendas, medium=reviewing calendar or event details, low=calendar visible but not being interacted with. Detect via: calendar apps, scheduling interfaces, event creation, imported calendar events, meeting invitations, agenda drafting. Use for scheduling and preparation work. Do not confuse with meeting โ planning is the preparation, meeting is the actual synchronous interaction.",
"name": "Planning"
},
{
"description": "Social media browsing and interaction",
- "icon": "๐ฑ",
+ "emoji": "๐ฑ",
+ "icon": "share-2",
+ "icon_svg": "
",
"id": "social",
"instructions": "Levels: high=posting or actively engaging, medium=scrolling feed, low=social app open but idle. Detect via: social media sites/apps, feed content, post composition.",
"name": "Social Media"
},
{
"description": "Spreadsheets, slides, and task management",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "chart-column",
+ "icon_svg": "
",
"id": "productivity",
"instructions": "Levels: high=actively editing or organizing, medium=reviewing data, low=app open but not focused. Detect via: spreadsheet/slide editors, project management tools, task boards.",
"name": "Productivity"
},
{
"description": "Watching videos and streaming content",
- "icon": "๐ฌ",
+ "emoji": "๐ฌ",
+ "icon": "clapperboard",
+ "icon_svg": "
",
"id": "video",
"instructions": "Levels: high=actively watching, medium=video playing while doing something else, low=video paused or minimized. Detect via: video player UI, streaming sites, playback controls.",
"name": "Video"
},
{
"description": "Web browsing, research, and reading online",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "globe",
+ "icon_svg": "
",
"id": "browsing",
"instructions": "Levels: high=actively navigating/researching, medium=reading a page, low=browser open but idle. Detect via: browser tabs, URL changes, search queries.",
"name": "Browsing"
},
{
"description": "a call you have planned",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "phone",
+ "icon_svg": "
",
"id": "call",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "call"
},
{
"description": "a celebration on the calendar",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "party-popper",
+ "icon_svg": "
",
"id": "celebration",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "celebration"
},
{
"description": "a deadline you are working toward",
- "icon": "โฐ",
+ "emoji": "โฐ",
+ "icon": "alarm-clock",
+ "icon_svg": "
",
"id": "deadline",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "deadline"
},
{
"description": "a medical appointment on your calendar",
- "icon": "๐ฉบ",
+ "emoji": "๐ฉบ",
+ "icon": "stethoscope",
+ "icon_svg": "
",
"id": "doctor_appointment",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "doctor appointment"
},
{
"description": "a reminder for something upcoming",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "bell",
+ "icon_svg": "
",
"id": "reminder",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "reminder"
},
{
"description": "an appointment on your calendar",
- "icon": "๐",
+ "emoji": "๐",
+ "icon": "pin",
+ "icon_svg": "
",
"id": "appointment",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "appointment"
},
{
"description": "an errand you plan to do",
- "icon": "๐งพ",
+ "emoji": "๐งพ",
+ "icon": "receipt",
+ "icon_svg": "
",
"id": "errand",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "errand"
},
{
"description": "an event you plan to attend",
- "icon": "๐๏ธ",
+ "emoji": "๐๏ธ",
+ "icon": "ticket",
+ "icon_svg": "
",
"id": "event",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "event"
},
{
"description": "travel you have planned",
- "icon": "โ๏ธ",
+ "emoji": "โ๏ธ",
+ "icon": "plane",
+ "icon_svg": "
",
"id": "travel",
"instructions": "Scheduled events emitted by talent/schedule.md; not detected from sense data.",
"name": "travel"
diff --git a/tests/test_activities.py b/tests/test_activities.py
index 2364d5bb0..0524597d4 100644
--- a/tests/test_activities.py
+++ b/tests/test_activities.py
@@ -25,6 +25,8 @@ def test_get_default_activities():
assert "id" in activity
assert "name" in activity
assert "description" in activity
+ assert "emoji" in activity
+ assert "icon" in activity
# Check some known activities exist
ids = [a["id"] for a in defaults]
@@ -39,6 +41,16 @@ def test_get_default_activities():
assert len(activity["instructions"]) > 0
+def test_default_activities_have_canonical_icon_shape():
+ """Default activities keep emoji fallback and explicit Lucide icon name."""
+ from solstone.think.activities import DEFAULT_ACTIVITIES, LUCIDE_ICON_NAME_RE
+
+ assert len(DEFAULT_ACTIVITIES) == 25
+ for activity in DEFAULT_ACTIVITIES:
+ assert activity["emoji"]
+ assert LUCIDE_ICON_NAME_RE.fullmatch(activity["icon"])
+
+
def test_get_default_activities_returns_copy():
"""Test that get_default_activities returns a copy, not the original."""
from solstone.think.activities import get_default_activities
@@ -204,6 +216,8 @@ def test_facet_activities_roundtrip(monkeypatch):
"description": "A custom activity",
"instructions": "Custom activity detection hints",
"custom": True,
+ "emoji": "๐ฏ",
+ "icon": "sparkles",
},
]
save_facet_activities("test_facet", activities)
@@ -243,6 +257,91 @@ def test_facet_activities_roundtrip(monkeypatch):
assert custom["custom"] is True
assert custom["name"] == "Custom"
assert custom["instructions"] == "Custom activity detection hints"
+ assert custom["emoji"] == "๐ฏ"
+ assert custom["icon"] == "sparkles"
+
+
+def test_get_facet_activities_normalizes_legacy_custom_icon_without_writing(
+ monkeypatch,
+):
+ """Legacy custom records with glyph in icon are normalized in memory only."""
+ from solstone.think.activities import get_facet_activities
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)
+ activities_dir = Path(tmpdir) / "facets" / "work" / "activities"
+ activities_dir.mkdir(parents=True)
+ activities_file = activities_dir / "activities.jsonl"
+ activities_file.write_text(
+ json.dumps(
+ {
+ "id": "legacy_custom",
+ "custom": True,
+ "name": "Legacy custom",
+ "description": "Old stored shape",
+ "icon": "๐ฏ",
+ }
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+
+ activities = get_facet_activities("work")
+
+ legacy = next(
+ activity for activity in activities if activity["id"] == "legacy_custom"
+ )
+ assert legacy["emoji"] == "๐ฏ"
+ assert "icon" not in legacy
+ assert '"icon":' in activities_file.read_text(encoding="utf-8")
+ assert '"emoji":' not in activities_file.read_text(encoding="utf-8")
+
+
+def test_migrate_custom_activity_icons_to_emoji_is_idempotent(monkeypatch):
+ from solstone.think.activities import migrate_custom_activity_icons_to_emoji
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)
+ activities_dir = Path(tmpdir) / "facets" / "work" / "activities"
+ activities_dir.mkdir(parents=True)
+ activities_file = activities_dir / "activities.jsonl"
+ activities_file.write_text(
+ json.dumps(
+ {
+ "id": "legacy_custom",
+ "custom": True,
+ "name": "Legacy custom",
+ "description": "Old stored shape",
+ "icon": "๐ฏ",
+ }
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+
+ first = migrate_custom_activity_icons_to_emoji()
+ second = migrate_custom_activity_icons_to_emoji()
+
+ assert first["files_scanned"] == 1
+ assert first["files_changed"] == 1
+ assert first["records_changed"] == 1
+ assert second["files_scanned"] == 1
+ assert second["files_changed"] == 0
+ assert second["records_changed"] == 0
+
+ records = [
+ json.loads(line)
+ for line in activities_file.read_text(encoding="utf-8").splitlines()
+ ]
+ assert records == [
+ {
+ "id": "legacy_custom",
+ "custom": True,
+ "name": "Legacy custom",
+ "description": "Old stored shape",
+ "emoji": "๐ฏ",
+ }
+ ]
def test_add_activity_to_facet(monkeypatch):
diff --git a/tests/test_app_activities.py b/tests/test_app_activities.py
index e27622c80..dc5270f61 100644
--- a/tests/test_app_activities.py
+++ b/tests/test_app_activities.py
@@ -8,10 +8,12 @@ import json
import pytest
from solstone.apps.activities.routes import (
- _GENERIC_ACTIVITY_ICON,
+ _GENERIC_ACTIVITY_EMOJI,
+ _GENERIC_ACTIVITY_LUCIDE,
_enrich_activity_record,
activities_bp,
)
+from solstone.convey.icons import lucide_svg
@pytest.fixture
@@ -63,7 +65,9 @@ class TestActivitiesDayRoutes:
data = resp.get_json()["items"]
coding = next(a for a in data if a["activity"] == "coding")
assert coding["name"] != ""
+ assert coding["emoji"] != ""
assert coding["icon"] != ""
+ assert "