@geminilight/mindos 0.5.63 → 0.5.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +4 -0
  2. package/README_zh.md +4 -0
  3. package/app/app/api/ask/route.ts +12 -0
  4. package/app/app/api/changes/route.ts +7 -1
  5. package/app/app/api/file/route.ts +9 -0
  6. package/app/app/api/mcp/agents/route.ts +27 -1
  7. package/app/app/api/mcp/install-skill/route.ts +9 -24
  8. package/app/app/api/skills/route.ts +18 -2
  9. package/app/app/api/tree-version/route.ts +8 -0
  10. package/app/app/layout.tsx +1 -0
  11. package/app/app/page.tsx +1 -2
  12. package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
  13. package/app/components/ActivityBar.tsx +2 -2
  14. package/app/components/Backlinks.tsx +5 -5
  15. package/app/components/CreateSpaceModal.tsx +3 -2
  16. package/app/components/DirPicker.tsx +1 -1
  17. package/app/components/DirView.tsx +2 -3
  18. package/app/components/EditorWrapper.tsx +3 -3
  19. package/app/components/FileTree.tsx +25 -10
  20. package/app/components/GuideCard.tsx +4 -4
  21. package/app/components/HomeContent.tsx +44 -14
  22. package/app/components/MarkdownView.tsx +2 -2
  23. package/app/components/OnboardingView.tsx +1 -1
  24. package/app/components/Panel.tsx +1 -1
  25. package/app/components/RightAgentDetailPanel.tsx +2 -1
  26. package/app/components/RightAskPanel.tsx +1 -1
  27. package/app/components/SearchModal.tsx +10 -2
  28. package/app/components/SidebarLayout.tsx +36 -10
  29. package/app/components/ThemeToggle.tsx +1 -1
  30. package/app/components/agents/AgentDetailContent.tsx +454 -59
  31. package/app/components/agents/AgentsContentPage.tsx +89 -20
  32. package/app/components/agents/AgentsMcpSection.tsx +513 -85
  33. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  34. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  35. package/app/components/agents/AgentsSkillsSection.tsx +746 -105
  36. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  37. package/app/components/agents/agents-content-model.ts +308 -10
  38. package/app/components/ask/AskContent.tsx +34 -5
  39. package/app/components/ask/FileChip.tsx +1 -0
  40. package/app/components/ask/MentionPopover.tsx +13 -1
  41. package/app/components/ask/MessageList.tsx +5 -7
  42. package/app/components/ask/ToolCallBlock.tsx +4 -4
  43. package/app/components/changes/ChangesBanner.tsx +89 -13
  44. package/app/components/changes/ChangesContentPage.tsx +134 -51
  45. package/app/components/echo/EchoHero.tsx +10 -24
  46. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  47. package/app/components/echo/EchoPageSections.tsx +13 -9
  48. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  49. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  50. package/app/components/explore/ExploreContent.tsx +3 -7
  51. package/app/components/explore/UseCaseCard.tsx +4 -15
  52. package/app/components/panels/AgentsPanel.tsx +22 -128
  53. package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
  54. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
  55. package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
  56. package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
  57. package/app/components/panels/EchoPanel.tsx +8 -10
  58. package/app/components/panels/PanelNavRow.tsx +9 -2
  59. package/app/components/panels/PluginsPanel.tsx +5 -5
  60. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  61. package/app/components/renderers/agent-inspector/manifest.ts +5 -3
  62. package/app/components/renderers/config/manifest.ts +1 -0
  63. package/app/components/renderers/csv/manifest.ts +1 -0
  64. package/app/components/renderers/todo/manifest.ts +1 -0
  65. package/app/components/settings/AiTab.tsx +3 -3
  66. package/app/components/settings/AppearanceTab.tsx +2 -2
  67. package/app/components/settings/KnowledgeTab.tsx +3 -3
  68. package/app/components/settings/McpAgentInstall.tsx +3 -6
  69. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  70. package/app/components/settings/McpSkillRow.tsx +2 -3
  71. package/app/components/settings/McpSkillsSection.tsx +2 -2
  72. package/app/components/settings/McpTab.tsx +12 -13
  73. package/app/components/settings/MonitoringTab.tsx +13 -13
  74. package/app/components/settings/PluginsTab.tsx +6 -5
  75. package/app/components/settings/Primitives.tsx +3 -4
  76. package/app/components/settings/SettingsContent.tsx +3 -3
  77. package/app/components/settings/SyncTab.tsx +11 -17
  78. package/app/components/settings/UpdateTab.tsx +18 -21
  79. package/app/components/settings/types.ts +14 -0
  80. package/app/components/setup/StepKB.tsx +1 -1
  81. package/app/hooks/useMcpData.tsx +7 -4
  82. package/app/hooks/useMention.ts +25 -8
  83. package/app/lib/agent/log.ts +15 -18
  84. package/app/lib/agent/stream-consumer.ts +3 -0
  85. package/app/lib/agent/to-agent-messages.ts +6 -4
  86. package/app/lib/core/agent-audit-log.ts +280 -0
  87. package/app/lib/core/content-changes.ts +148 -8
  88. package/app/lib/core/index.ts +11 -0
  89. package/app/lib/fs.ts +16 -1
  90. package/app/lib/i18n-en.ts +317 -36
  91. package/app/lib/i18n-zh.ts +316 -35
  92. package/app/lib/mcp-agents.ts +273 -2
  93. package/app/lib/renderers/index.ts +1 -2
  94. package/app/lib/renderers/registry.ts +10 -0
  95. package/app/lib/types.ts +2 -0
  96. package/app/next-env.d.ts +1 -1
  97. package/bin/lib/mcp-agents.js +38 -13
  98. package/package.json +1 -1
  99. package/scripts/migrate-agent-audit-log.js +170 -0
  100. package/scripts/migrate-agent-diff.js +146 -0
  101. package/scripts/setup.js +12 -17
  102. package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
  103. package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
  104. package/app/components/renderers/diff/manifest.ts +0 -14
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { ChevronDown, ChevronRight, History, RefreshCw } from 'lucide-react';
6
6
  import { apiFetch } from '@/lib/api';
7
+ import { useLocale } from '@/lib/LocaleContext';
7
8
  import { collapseDiffContext, buildLineDiff } from './line-diff';
8
9
 
9
10
  interface ChangeEvent {
@@ -28,18 +29,22 @@ interface ListPayload {
28
29
  events: ChangeEvent[];
29
30
  }
30
31
 
31
- function relativeTime(ts: string): string {
32
+ function relativeTime(ts: string, t: ReturnType<typeof useLocale>['t']): string {
32
33
  const delta = Date.now() - new Date(ts).getTime();
33
34
  const mins = Math.floor(delta / 60000);
34
- if (mins < 1) return 'just now';
35
- if (mins < 60) return `${mins}m ago`;
35
+ if (mins < 1) return t.changes.relativeTime.justNow;
36
+ if (mins < 60) return t.changes.relativeTime.minutesAgo(mins);
36
37
  const hours = Math.floor(mins / 60);
37
- if (hours < 24) return `${hours}h ago`;
38
- return `${Math.floor(hours / 24)}d ago`;
38
+ if (hours < 24) return t.changes.relativeTime.hoursAgo(hours);
39
+ return t.changes.relativeTime.daysAgo(Math.floor(hours / 24));
39
40
  }
40
41
 
41
42
  export default function ChangesContentPage({ initialPath = '' }: { initialPath?: string }) {
43
+ const { t } = useLocale();
42
44
  const [pathFilter, setPathFilter] = useState(initialPath);
45
+ const [sourceFilter, setSourceFilter] = useState<'all' | 'agent' | 'user' | 'system'>('all');
46
+ const [opFilter, setOpFilter] = useState<string>('all');
47
+ const [queryFilter, setQueryFilter] = useState('');
43
48
  const [events, setEvents] = useState<ChangeEvent[]>([]);
44
49
  const [summary, setSummary] = useState<SummaryPayload>({ unreadCount: 0, totalCount: 0 });
45
50
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
@@ -50,9 +55,12 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
50
55
  setLoading(true);
51
56
  setError(null);
52
57
  try {
53
- const listUrl = pathFilter
54
- ? `/api/changes?op=list&limit=80&path=${encodeURIComponent(pathFilter)}`
55
- : '/api/changes?op=list&limit=80';
58
+ const params = new URLSearchParams({ op: 'list', limit: '120' });
59
+ if (pathFilter.trim()) params.set('path', pathFilter.trim());
60
+ if (sourceFilter !== 'all') params.set('source', sourceFilter);
61
+ if (opFilter !== 'all') params.set('event_op', opFilter);
62
+ if (queryFilter.trim()) params.set('q', queryFilter.trim());
63
+ const listUrl = `/api/changes?${params.toString()}`;
56
64
  const [list, summaryData] = await Promise.all([
57
65
  apiFetch<ListPayload>(listUrl),
58
66
  apiFetch<SummaryPayload>('/api/changes?op=summary'),
@@ -64,7 +72,7 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
64
72
  } finally {
65
73
  setLoading(false);
66
74
  }
67
- }, [pathFilter]);
75
+ }, [pathFilter, sourceFilter, opFilter, queryFilter]);
68
76
 
69
77
  useEffect(() => {
70
78
  void fetchData();
@@ -79,55 +87,116 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
79
87
  await fetchData();
80
88
  }, [fetchData]);
81
89
 
82
- const eventCountLabel = useMemo(() => `${events.length} event${events.length === 1 ? '' : 's'}`, [events.length]);
90
+ const eventCountLabel = useMemo(() => t.changes.eventsCount(events.length), [events.length, t]);
91
+ const opOptions = useMemo(() => {
92
+ const ops = Array.from(new Set(events.map((e) => e.op))).sort((a, b) => a.localeCompare(b));
93
+ if (opFilter !== 'all' && !ops.includes(opFilter)) ops.unshift(opFilter);
94
+ return ['all', ...ops];
95
+ }, [events, opFilter]);
96
+
97
+ const sourceLabel = useCallback((source: ChangeEvent['source']) => {
98
+ if (source === 'agent') return t.changes.filters.agent;
99
+ if (source === 'user') return t.changes.filters.user;
100
+ return t.changes.filters.system;
101
+ }, [t]);
83
102
 
84
103
  return (
85
104
  <div className="min-h-screen">
86
- <div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5 bg-background">
87
- <div className="content-width xl:mr-[220px] flex items-center justify-between gap-3">
88
- <div className="min-w-0">
89
- <div className="flex items-center gap-2 text-sm font-medium text-foreground font-display">
90
- <History size={15} />
91
- Content changes
105
+ <div className="px-4 md:px-6 pt-6 md:pt-8">
106
+ <div className="content-width xl:mr-[220px] rounded-xl border border-border bg-card px-4 py-3 md:px-5 md:py-4">
107
+ <div className="flex flex-wrap items-start justify-between gap-3">
108
+ <div className="min-w-0">
109
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground font-display">
110
+ <History size={15} />
111
+ {t.changes.title}
112
+ </div>
113
+ <p className="mt-1 text-xs text-muted-foreground">
114
+ {t.changes.subtitle}
115
+ </p>
116
+ <div className="mt-2 flex items-center gap-2 text-xs">
117
+ <span className="rounded-full bg-muted px-2 py-0.5 text-muted-foreground">{eventCountLabel}</span>
118
+ <span className="rounded-full bg-[var(--amber-dim)] px-2 py-0.5 text-[var(--amber)]">
119
+ {t.changes.unreadCount(summary.unreadCount)}
120
+ </span>
121
+ </div>
122
+ </div>
123
+ <div className="flex items-center gap-2">
124
+ <button
125
+ type="button"
126
+ onClick={() => void fetchData()}
127
+ className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
128
+ >
129
+ <span className="inline-flex items-center gap-1"><RefreshCw size={12} /> {t.changes.refresh}</span>
130
+ </button>
131
+ <button
132
+ type="button"
133
+ onClick={() => void markSeen()}
134
+ className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
135
+ >
136
+ {t.changes.markAllRead}
137
+ </button>
92
138
  </div>
93
- <div className="text-xs text-muted-foreground mt-1">{eventCountLabel} · {summary.unreadCount} unread</div>
94
- </div>
95
- <div className="flex items-center gap-2">
96
- <button
97
- type="button"
98
- onClick={() => void fetchData()}
99
- className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
100
- >
101
- <span className="inline-flex items-center gap-1"><RefreshCw size={12} /> Refresh</span>
102
- </button>
103
- <button
104
- type="button"
105
- onClick={() => void markSeen()}
106
- className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)] focus-visible:ring-2 focus-visible:ring-ring"
107
- >
108
- Mark seen
109
- </button>
110
139
  </div>
111
140
  </div>
112
141
  </div>
113
142
 
114
- <div className="px-4 md:px-6 py-6 md:py-8">
143
+ <div className="px-4 md:px-6 py-4 md:py-6">
115
144
  <div className="content-width xl:mr-[220px] space-y-3">
116
145
  <div className="rounded-lg border border-border bg-card p-3">
117
- <label className="text-xs text-muted-foreground">Filter by file path</label>
118
- <input
119
- value={pathFilter}
120
- onChange={(e) => setPathFilter(e.target.value)}
121
- placeholder="e.g. Projects/plan.md"
122
- className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
123
- />
146
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-2.5">
147
+ <label className="block">
148
+ <span className="text-xs text-muted-foreground font-display">{t.changes.filters.filePath}</span>
149
+ <input
150
+ value={pathFilter}
151
+ onChange={(e) => setPathFilter(e.target.value)}
152
+ placeholder={t.changes.filters.filePathPlaceholder}
153
+ className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
154
+ />
155
+ </label>
156
+ <label className="block">
157
+ <span className="text-xs text-muted-foreground font-display">{t.changes.filters.source}</span>
158
+ <select
159
+ value={sourceFilter}
160
+ onChange={(e) => setSourceFilter(e.target.value as 'all' | 'agent' | 'user' | 'system')}
161
+ className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
162
+ >
163
+ <option value="all">{t.changes.filters.all}</option>
164
+ <option value="agent">{t.changes.filters.agent}</option>
165
+ <option value="user">{t.changes.filters.user}</option>
166
+ <option value="system">{t.changes.filters.system}</option>
167
+ </select>
168
+ </label>
169
+ <label className="block">
170
+ <span className="text-xs text-muted-foreground font-display">{t.changes.filters.operation}</span>
171
+ <select
172
+ value={opFilter}
173
+ onChange={(e) => setOpFilter(e.target.value)}
174
+ className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
175
+ >
176
+ {opOptions.map((op) => (
177
+ <option key={op} value={op}>
178
+ {op === 'all' ? t.changes.filters.operationAll : op}
179
+ </option>
180
+ ))}
181
+ </select>
182
+ </label>
183
+ <label className="block">
184
+ <span className="text-xs text-muted-foreground font-display">{t.changes.filters.keyword}</span>
185
+ <input
186
+ value={queryFilter}
187
+ onChange={(e) => setQueryFilter(e.target.value)}
188
+ placeholder={t.changes.filters.keywordPlaceholder}
189
+ className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
190
+ />
191
+ </label>
192
+ </div>
124
193
  </div>
125
194
 
126
- {loading && <p className="text-sm text-muted-foreground">Loading changes...</p>}
195
+ {loading && <p className="text-sm text-muted-foreground">{t.changes.loading}</p>}
127
196
  {error && <p className="text-sm text-error">{error}</p>}
128
197
  {!loading && !error && events.length === 0 && (
129
198
  <div className="rounded-lg border border-border bg-card p-6 text-center text-sm text-muted-foreground">
130
- No content changes yet.
199
+ {t.changes.empty}
131
200
  </div>
132
201
  )}
133
202
 
@@ -135,35 +204,49 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
135
204
  const open = !!expanded[event.id];
136
205
  const rows = collapseDiffContext(buildLineDiff(event.before ?? '', event.after ?? ''));
137
206
  return (
138
- <div key={event.id} className="rounded-lg border border-border bg-card overflow-hidden">
207
+ <div key={event.id} className="rounded-xl border border-border bg-card overflow-hidden">
139
208
  <button
140
209
  type="button"
141
210
  onClick={() => setExpanded((prev) => ({ ...prev, [event.id]: !prev[event.id] }))}
142
- className="w-full px-3 py-2.5 text-left hover:bg-muted/30 focus-visible:ring-2 focus-visible:ring-ring"
211
+ className="w-full px-3 py-3 text-left hover:bg-muted/30 focus-visible:ring-2 focus-visible:ring-ring"
143
212
  >
144
213
  <div className="flex items-start gap-2">
145
214
  <span className="pt-0.5 text-muted-foreground">{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}</span>
146
215
  <div className="min-w-0 flex-1">
147
216
  <div className="text-sm font-medium text-foreground font-display">{event.summary}</div>
148
- <div className="text-xs text-muted-foreground mt-0.5">
149
- {event.path} · {event.op} · {event.source} · {relativeTime(event.ts)}
217
+ <div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
218
+ <span
219
+ className="rounded-md px-2 py-0.5 font-medium"
220
+ style={{
221
+ background: 'color-mix(in srgb, var(--amber) 16%, var(--muted))',
222
+ color: 'var(--foreground)',
223
+ border: '1px solid color-mix(in srgb, var(--amber) 36%, var(--border))',
224
+ }}
225
+ >
226
+ {event.path}
227
+ </span>
228
+ <span>{event.op}</span>
229
+ <span>·</span>
230
+ <span>{sourceLabel(event.source)}</span>
231
+ <span>·</span>
232
+ <span>{relativeTime(event.ts, t)}</span>
150
233
  </div>
151
234
  </div>
152
235
  <Link
153
236
  href={`/view/${event.path.split('/').map(encodeURIComponent).join('/')}`}
154
- className="text-xs text-[var(--amber)] hover:underline"
237
+ className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber)] focus-visible:ring-2 focus-visible:ring-ring hover:opacity-90"
155
238
  onClick={(e) => e.stopPropagation()}
156
239
  >
157
- Open
240
+ {t.changes.open}
158
241
  </Link>
159
242
  </div>
160
243
  </button>
161
244
 
162
245
  {open && (
163
- <div className="border-t border-border bg-background">
246
+ <div className="border-t border-border bg-background/70">
164
247
  {rows.map((row, idx) => {
165
248
  if (row.type === 'gap') {
166
- return <div key={`${event.id}-gap-${idx}`} className="px-3 py-1 text-2xs text-muted-foreground">... {row.count} unchanged lines ...</div>;
249
+ return <div key={`${event.id}-gap-${idx}`} className="px-3 py-1 text-2xs text-muted-foreground">{t.changes.unchangedLines(row.count)}</div>;
167
250
  }
168
251
  const prefix = row.type === 'insert' ? '+' : row.type === 'delete' ? '-' : ' ';
169
252
  const color = row.type === 'insert'
@@ -1,55 +1,41 @@
1
1
  'use client';
2
2
 
3
- import Link from 'next/link';
3
+ import type { ReactNode } from 'react';
4
4
 
5
5
  /**
6
- * Echo page hero: kicker, minimal breadcrumb (parent only h1 holds the section title),
7
- * lead. Avoids repeating the current segment name in both breadcrumb and h1.
6
+ * Echo page hero: kicker, h1, lead, and optional embedded children (e.g. segment nav).
7
+ * The accent bar highlights the text zone; children sit below it inside the card.
8
8
  */
9
9
  export function EchoHero({
10
- breadcrumbNav,
11
- parentHref,
12
- parent,
13
10
  heroKicker,
14
11
  pageTitle,
15
12
  lead,
16
13
  titleId,
14
+ children,
17
15
  }: {
18
- breadcrumbNav: string;
19
- parentHref: string;
20
- parent: string;
21
16
  heroKicker: string;
22
17
  pageTitle: string;
23
18
  lead: string;
24
19
  titleId: string;
20
+ children?: ReactNode;
25
21
  }) {
26
22
  return (
27
- <header className="relative overflow-hidden rounded-xl border border-border bg-card px-5 py-6 shadow-sm sm:px-8 sm:py-8">
23
+ <header className="relative overflow-hidden rounded-xl border border-border bg-card px-5 pb-5 pt-6 shadow-sm sm:px-8 sm:pb-6 sm:pt-8">
28
24
  <div
29
- className="absolute bottom-5 left-0 top-5 w-0.5 rounded-full bg-[var(--amber)] sm:bottom-6 sm:top-6"
25
+ className="absolute left-0 top-5 w-[3px] rounded-full bg-[var(--amber)] sm:top-6"
26
+ style={{ bottom: children ? '40%' : '1.25rem' }}
30
27
  aria-hidden
31
28
  />
32
29
  <div className="relative pl-4 sm:pl-5">
33
- <p className="mb-3 font-sans text-2xs font-semibold uppercase tracking-[0.2em] text-[var(--amber)]">
30
+ <p className="mb-4 font-sans text-2xs font-semibold uppercase tracking-[0.2em] text-[var(--amber)]">
34
31
  {heroKicker}
35
32
  </p>
36
- <nav aria-label={breadcrumbNav} className="mb-5 font-sans text-sm">
37
- <ol className="m-0 list-none p-0">
38
- <li>
39
- <Link
40
- href={parentHref}
41
- className="text-muted-foreground transition-colors duration-150 hover:text-[var(--amber)] focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
42
- >
43
- {parent}
44
- </Link>
45
- </li>
46
- </ol>
47
- </nav>
48
33
  <h1 id={titleId} className="font-display text-2xl font-semibold tracking-tight text-foreground md:text-3xl">
49
34
  {pageTitle}
50
35
  </h1>
51
36
  <p className="mt-3 max-w-prose font-sans text-base leading-relaxed text-muted-foreground">{lead}</p>
52
37
  </div>
38
+ {children}
53
39
  </header>
54
40
  );
55
41
  }
@@ -103,7 +103,7 @@ export function EchoInsightCollapsible({
103
103
  const generateDisabled = aiLoading || !aiReady || streaming;
104
104
 
105
105
  return (
106
- <div className="mt-10 overflow-hidden rounded-xl border border-border bg-card shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/15 hover:shadow-md">
106
+ <div className="mt-10 overflow-hidden rounded-xl border border-border bg-card shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/15 hover:shadow">
107
107
  <button
108
108
  id={btnId}
109
109
  type="button"
@@ -129,56 +129,65 @@ export function EchoInsightCollapsible({
129
129
  />
130
130
  <span className="sr-only">{open ? hideLabel : showLabel}</span>
131
131
  </button>
132
- {open ? (
133
- <div
134
- id={panelId}
135
- role="region"
136
- aria-labelledby={btnId}
137
- className="border-t border-border/60 px-5 pb-5 pt-4"
138
- >
139
- <p className="font-sans text-sm leading-relaxed text-muted-foreground">{hint}</p>
140
- <div className="mt-4 flex flex-wrap items-center gap-2">
141
- <button
142
- type="button"
143
- disabled={generateDisabled}
144
- onClick={runGenerate}
145
- className="inline-flex items-center gap-2 rounded-lg bg-[var(--amber)] px-3 py-2 font-sans text-sm font-medium text-white transition-opacity duration-150 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
146
- >
147
- {streaming ? <Loader2 size={16} className="animate-spin shrink-0" aria-hidden /> : null}
148
- {streaming ? generatingLabel : generateLabel}
149
- </button>
150
- {err ? (
132
+ <div
133
+ id={panelId}
134
+ role="region"
135
+ aria-labelledby={btnId}
136
+ className={cn(
137
+ 'grid transition-[grid-template-rows] duration-250 ease-out',
138
+ open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
139
+ )}
140
+ >
141
+ <div className="overflow-hidden">
142
+ <div className="border-t border-border/60 px-5 pb-5 pt-4">
143
+ <p className="font-sans text-sm leading-relaxed text-muted-foreground">{hint}</p>
144
+ <div className="mt-4 flex flex-wrap items-center gap-2">
151
145
  <button
152
146
  type="button"
147
+ disabled={generateDisabled}
153
148
  onClick={runGenerate}
154
- disabled={streaming || !aiReady}
155
- className="font-sans text-sm text-[var(--amber)] underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
149
+ className="inline-flex items-center gap-2 rounded-lg bg-[var(--amber)] px-3 py-2 font-sans text-sm font-medium text-[var(--amber-foreground)] transition-opacity duration-150 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
156
150
  >
157
- {retryLabel}
151
+ {streaming ? (
152
+ <Loader2 size={16} className="animate-spin shrink-0" aria-hidden />
153
+ ) : (
154
+ <Sparkles size={15} className="shrink-0" aria-hidden />
155
+ )}
156
+ {streaming ? generatingLabel : generateLabel}
158
157
  </button>
159
- ) : null}
160
- </div>
161
- {!aiLoading && !aiReady ? (
162
- <p className="mt-2 font-sans text-2xs text-muted-foreground">{noAiHint}</p>
163
- ) : null}
164
- {err ? (
165
- <p className="mt-3 font-sans text-sm text-error" role="alert">
166
- {errorPrefix} {err}
167
- </p>
168
- ) : null}
169
- {insightMd ? (
170
- <div className={cn(proseInsight, 'mt-4 border-t border-border/60 pt-4')}>
171
- <ReactMarkdown remarkPlugins={[remarkGfm]}>{insightMd}</ReactMarkdown>
172
- {streaming ? (
173
- <span
174
- className="ml-0.5 inline-block h-3.5 w-1 animate-pulse rounded-sm bg-[var(--amber)] align-middle"
175
- aria-hidden
176
- />
158
+ {err ? (
159
+ <button
160
+ type="button"
161
+ onClick={runGenerate}
162
+ disabled={streaming || !aiReady}
163
+ className="font-sans text-sm text-[var(--amber)] underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
164
+ >
165
+ {retryLabel}
166
+ </button>
177
167
  ) : null}
178
168
  </div>
179
- ) : null}
169
+ {!aiLoading && !aiReady ? (
170
+ <p className="mt-2 font-sans text-2xs text-muted-foreground">{noAiHint}</p>
171
+ ) : null}
172
+ {err ? (
173
+ <p className="mt-3 font-sans text-sm text-error" role="alert">
174
+ {errorPrefix} {err}
175
+ </p>
176
+ ) : null}
177
+ {insightMd ? (
178
+ <div className={cn(proseInsight, 'mt-4 border-t border-border/60 pt-4')}>
179
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{insightMd}</ReactMarkdown>
180
+ {streaming ? (
181
+ <span
182
+ className="ml-0.5 inline-block h-3.5 w-1 animate-pulse rounded-sm bg-[var(--amber)] align-middle"
183
+ aria-hidden
184
+ />
185
+ ) : null}
186
+ </div>
187
+ ) : null}
188
+ </div>
180
189
  </div>
181
- ) : null}
190
+ </div>
182
191
  </div>
183
192
  );
184
193
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type { ReactNode } from 'react';
4
- import { Library } from 'lucide-react';
4
+ import { Library, FileText, CircleCheck } from 'lucide-react';
5
5
 
6
6
  export function EchoFactSnapshot({
7
7
  headingId,
@@ -9,6 +9,7 @@ export function EchoFactSnapshot({
9
9
  snapshotBadge,
10
10
  emptyTitle,
11
11
  emptyBody,
12
+ icon,
12
13
  actions,
13
14
  }: {
14
15
  headingId: string;
@@ -16,12 +17,12 @@ export function EchoFactSnapshot({
16
17
  snapshotBadge: string;
17
18
  emptyTitle: string;
18
19
  emptyBody: string;
19
- /** e.g. continue-in-Agent CTA — inside this card so it is not orphaned between sections */
20
+ icon?: ReactNode;
20
21
  actions?: ReactNode;
21
22
  }) {
22
23
  return (
23
24
  <section
24
- className="rounded-xl border border-border bg-card p-5 shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/20 hover:shadow-md sm:p-6"
25
+ className="rounded-xl border border-border bg-card p-5 shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/20 hover:shadow sm:p-6"
25
26
  aria-labelledby={headingId}
26
27
  >
27
28
  <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
@@ -30,7 +31,7 @@ export function EchoFactSnapshot({
30
31
  className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--amber-dim)] text-[var(--amber)]"
31
32
  aria-hidden
32
33
  >
33
- <Library size={18} strokeWidth={1.75} />
34
+ {icon ?? <Library size={18} strokeWidth={1.75} />}
34
35
  </span>
35
36
  <div>
36
37
  <h2
@@ -67,9 +68,12 @@ export function EchoContinuedGroups({
67
68
  subEmptyHint: string;
68
69
  footer?: ReactNode;
69
70
  }) {
70
- const cell = (label: string) => (
71
- <div className="flex min-h-[5.75rem] flex-col justify-center rounded-xl border border-dashed border-border/80 bg-muted/10 px-4 py-4">
72
- <h3 className="font-sans text-sm font-medium text-foreground">{label}</h3>
71
+ const cell = (label: string, icon: ReactNode) => (
72
+ <div className="flex min-h-[5.75rem] flex-col justify-center rounded-xl border border-dashed border-border/80 bg-muted/10 px-4 py-4 transition-colors duration-150 hover:border-[var(--amber)]/25 hover:bg-[var(--amber-dim)]/15">
73
+ <div className="flex items-center gap-2">
74
+ <span className="shrink-0 text-muted-foreground" aria-hidden>{icon}</span>
75
+ <h3 className="font-sans text-sm font-medium text-foreground">{label}</h3>
76
+ </div>
73
77
  <p className="mt-2 font-sans text-2xs leading-relaxed text-muted-foreground">{subEmptyHint}</p>
74
78
  </div>
75
79
  );
@@ -77,8 +81,8 @@ export function EchoContinuedGroups({
77
81
  return (
78
82
  <div className="space-y-4">
79
83
  <div className="grid gap-3 sm:grid-cols-2">
80
- {cell(draftsLabel)}
81
- {cell(todosLabel)}
84
+ {cell(draftsLabel, <FileText size={15} strokeWidth={1.75} />)}
85
+ {cell(todosLabel, <CircleCheck size={15} strokeWidth={1.75} />)}
82
86
  </div>
83
87
  {footer ? <div className="border-t border-border/60 pt-4">{footer}</div> : null}
84
88
  </div>
@@ -1,25 +1,27 @@
1
1
  'use client';
2
2
 
3
+ import type { ReactNode } from 'react';
3
4
  import Link from 'next/link';
5
+ import { UserRound, Bookmark, Sun, History, Brain } from 'lucide-react';
4
6
  import { useLocale } from '@/lib/LocaleContext';
5
7
  import { cn } from '@/lib/utils';
6
8
  import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
7
9
 
8
- function labelForSegment(
10
+ function segmentMeta(
9
11
  segment: EchoSegment,
10
12
  echo: ReturnType<typeof useLocale>['t']['panels']['echo'],
11
- ): string {
13
+ ): { label: string; icon: ReactNode } {
12
14
  switch (segment) {
13
15
  case 'about-you':
14
- return echo.aboutYouTitle;
16
+ return { label: echo.aboutYouTitle, icon: <UserRound size={13} /> };
15
17
  case 'continued':
16
- return echo.continuedTitle;
18
+ return { label: echo.continuedTitle, icon: <Bookmark size={13} /> };
17
19
  case 'daily':
18
- return echo.dailyEchoTitle;
20
+ return { label: echo.dailyEchoTitle, icon: <Sun size={13} /> };
19
21
  case 'past-you':
20
- return echo.pastYouTitle;
22
+ return { label: echo.pastYouTitle, icon: <History size={13} /> };
21
23
  case 'growth':
22
- return echo.intentGrowthTitle;
24
+ return { label: echo.intentGrowthTitle, icon: <Brain size={13} /> };
23
25
  }
24
26
  }
25
27
 
@@ -29,11 +31,11 @@ export default function EchoSegmentNav({ activeSegment }: { activeSegment: EchoS
29
31
  const aria = t.echoPages.segmentNavAria;
30
32
 
31
33
  return (
32
- <nav aria-label={aria} className="mt-6 font-sans">
33
- <ul className="-mx-1 flex snap-x snap-mandatory gap-1.5 overflow-x-auto px-1 pb-1 [scrollbar-width:thin]">
34
+ <nav aria-label={aria} className="mt-5 border-t border-border/30 pt-4 font-sans">
35
+ <ul className="flex snap-x snap-mandatory gap-1.5 overflow-x-auto pb-0.5 [scrollbar-width:thin]">
34
36
  {ECHO_SEGMENT_ORDER.map((segment) => {
35
37
  const href = ECHO_SEGMENT_HREF[segment];
36
- const label = labelForSegment(segment, echo);
38
+ const { label, icon } = segmentMeta(segment, echo);
37
39
  const isActive = segment === activeSegment;
38
40
  return (
39
41
  <li key={segment} className="snap-start shrink-0">
@@ -41,12 +43,13 @@ export default function EchoSegmentNav({ activeSegment }: { activeSegment: EchoS
41
43
  href={href}
42
44
  aria-current={isActive ? 'page' : undefined}
43
45
  className={cn(
44
- 'inline-flex min-h-9 max-w-[11rem] items-center rounded-full border px-3 py-1.5 text-sm transition-[background-color,border-color,color] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
46
+ 'inline-flex min-h-9 max-w-[11rem] items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm transition-[background-color,border-color,color] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
45
47
  isActive
46
48
  ? 'border-[var(--amber)]/45 bg-[var(--amber-dim)]/50 font-medium text-foreground'
47
49
  : 'border-transparent bg-muted/35 text-muted-foreground hover:bg-muted/55 hover:text-foreground',
48
50
  )}
49
51
  >
52
+ <span className="shrink-0" aria-hidden>{icon}</span>
50
53
  <span className="truncate">{label}</span>
51
54
  </Link>
52
55
  </li>