diff --git a/apps/x/apps/renderer/src/extensions/track-block.tsx b/apps/x/apps/renderer/src/extensions/track-block.tsx
index 536356e8..4f2a1f0a 100644
--- a/apps/x/apps/renderer/src/extensions/track-block.tsx
+++ b/apps/x/apps/renderer/src/extensions/track-block.tsx
@@ -1,18 +1,28 @@
import { z } from 'zod'
-import { useMemo } from 'react'
+import { useMemo, type ComponentType } from 'react'
import { mergeAttributes, Node } from '@tiptap/react'
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
-import { Radio, Loader2, CalendarDays, Mail, Clock, ListTodo, History } from 'lucide-react'
+import { Radio, Loader2, type LucideProps } from 'lucide-react'
+import * as LucideIcons from 'lucide-react'
import { parse as parseYaml } from 'yaml'
import { TrackBlockSchema } from '@x/shared/dist/track-block.js'
import { useTrackStatus } from '@/hooks/use-track-status'
-function TrackIcon({ trackId, size }: { trackId: string; size: number }) {
- if (trackId === 'up-next') return
- if (trackId === 'calendar') return
- if (trackId === 'emails') return
- if (trackId === 'what-you-missed') return
- if (trackId === 'priorities') return
+function resolveIcon(iconName: string): ComponentType | null {
+ const key = iconName
+ .split('-')
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
+ .join('')
+ const component = (LucideIcons as Record)[key]
+ if (component != null) return component as ComponentType
+ return null
+}
+
+function TrackIcon({ icon, size }: { icon?: string; size: number }) {
+ if (icon) {
+ const Icon = resolveIcon(icon)
+ if (Icon) return
+ }
return
}
@@ -109,7 +119,7 @@ function TrackBlockView({ node, deleteNode, extension }: {
{isRunning
?
- : }
+ : }
{trackId || 'track'}
{instruction && ·}
diff --git a/apps/x/packages/core/src/knowledge/ensure_daily_note.ts b/apps/x/packages/core/src/knowledge/ensure_daily_note.ts
index 00f003f9..a4da6977 100644
--- a/apps/x/packages/core/src/knowledge/ensure_daily_note.ts
+++ b/apps/x/packages/core/src/knowledge/ensure_daily_note.ts
@@ -18,6 +18,7 @@ const SECTIONS: Section[] = [
heading: '## Up Next',
track: {
trackId: 'up-next',
+ icon: 'clock',
instruction:
`Write 1-3 sentences of plain markdown giving the user a shoulder-tap about what's next on their calendar today.
@@ -44,6 +45,7 @@ Plain markdown prose only — no calendar block, no email block, no headings.`,
heading: '## Calendar',
track: {
trackId: 'calendar',
+ icon: 'calendar-days',
instruction:
`Emit today's meetings as a calendar block titled "Today's Meetings".
@@ -63,6 +65,7 @@ After the block, you MAY add one short markdown line per event giving useful pre
heading: '## Emails',
track: {
trackId: 'emails',
+ icon: 'mail',
instruction:
`Maintain a digest of email threads worth the user's attention today, rendered as zero or more email blocks (one per thread).
@@ -84,6 +87,7 @@ Do NOT re-list threads the user has already seen unless their state changed (new
heading: '## What You Missed',
track: {
trackId: 'what-you-missed',
+ icon: 'history',
instruction:
`Short markdown summary of what happened yesterday that matters this morning.
@@ -109,6 +113,7 @@ Do NOT manufacture content to fill the section.`,
heading: '## Today\'s Priorities',
track: {
trackId: 'priorities',
+ icon: 'list-todo',
instruction:
`Ranked markdown list of the real, actionable items the user should focus on today.
diff --git a/apps/x/packages/shared/src/track-block.ts b/apps/x/packages/shared/src/track-block.ts
index 6d9ce3af..cbb370cc 100644
--- a/apps/x/packages/shared/src/track-block.ts
+++ b/apps/x/packages/shared/src/track-block.ts
@@ -27,6 +27,7 @@ export const TrackBlockSchema = z.object({
schedule: TrackScheduleSchema.optional(),
model: z.string().optional().describe('ADVANCED — leave unset. Per-track LLM model override (e.g. "anthropic/claude-sonnet-4.6"). Only set when the user explicitly asked for a specific model for THIS track. The global default already picks a tuned model for tracks; overriding usually makes things worse, not better.'),
provider: z.string().optional().describe('ADVANCED — leave unset. Per-track provider name override (e.g. "openai", "anthropic"). Only set when the user explicitly asked for a specific provider for THIS track. Almost always omitted; the global default flows through correctly.'),
+ icon: z.string().optional().describe('Lucide icon name for the chip (e.g. "clock", "calendar-days", "mail", "history", "list-todo"). Omit to use the default icon for this track.'),
lastRunAt: z.string().optional().describe('Runtime-managed — never write this yourself'),
lastRunId: z.string().optional().describe('Runtime-managed — never write this yourself'),
lastRunSummary: z.string().optional().describe('Runtime-managed — never write this yourself'),