@geminilight/mindos 0.5.52 → 0.5.55

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 (32) hide show
  1. package/README.md +7 -7
  2. package/README_zh.md +5 -5
  3. package/app/app/echo/[segment]/page.tsx +15 -0
  4. package/app/app/echo/page.tsx +6 -0
  5. package/app/components/ActivityBar.tsx +3 -2
  6. package/app/components/Panel.tsx +1 -0
  7. package/app/components/RightAgentDetailPanel.tsx +121 -0
  8. package/app/components/RightAskPanel.tsx +14 -11
  9. package/app/components/SidebarLayout.tsx +69 -5
  10. package/app/components/ask/AskContent.tsx +10 -2
  11. package/app/components/echo/EchoHero.tsx +55 -0
  12. package/app/components/echo/EchoInsightCollapsible.tsx +184 -0
  13. package/app/components/echo/EchoPageSections.tsx +86 -0
  14. package/app/components/echo/EchoSegmentNav.tsx +58 -0
  15. package/app/components/echo/EchoSegmentPageClient.tsx +265 -0
  16. package/app/components/panels/AgentsPanel.tsx +156 -178
  17. package/app/components/panels/AgentsPanelAgentDetail.tsx +193 -0
  18. package/app/components/panels/AgentsPanelAgentGroups.tsx +116 -0
  19. package/app/components/panels/AgentsPanelAgentListRow.tsx +101 -0
  20. package/app/components/panels/AgentsPanelHubNav.tsx +48 -0
  21. package/app/components/panels/DiscoverPanel.tsx +6 -46
  22. package/app/components/panels/EchoPanel.tsx +49 -0
  23. package/app/components/panels/PanelNavRow.tsx +68 -0
  24. package/app/components/panels/agents-panel-resolve-status.ts +13 -0
  25. package/app/hooks/useSettingsAiAvailable.ts +29 -0
  26. package/app/lib/echo-insight-prompt.ts +44 -0
  27. package/app/lib/echo-segments.ts +27 -0
  28. package/app/lib/i18n-en.ts +62 -2
  29. package/app/lib/i18n-zh.ts +59 -2
  30. package/app/lib/settings-ai-client.ts +26 -0
  31. package/app/next-env.d.ts +1 -1
  32. package/package.json +1 -1
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import { ChevronDown, ChevronRight } from 'lucide-react';
4
+ import type { AgentInfo } from '../settings/types';
5
+ import type { McpContextValue } from '@/hooks/useMcpData';
6
+ import AgentsPanelAgentListRow, { type AgentsPanelAgentListRowCopy } from './AgentsPanelAgentListRow';
7
+
8
+ type AgentsCopy = {
9
+ rosterLabel: string;
10
+ sectionConnected: string;
11
+ sectionDetected: string;
12
+ sectionNotDetected: string;
13
+ };
14
+
15
+ export function AgentsPanelAgentGroups({
16
+ connected,
17
+ detected,
18
+ notFound,
19
+ onOpenDetail,
20
+ selectedAgentKey,
21
+ mcp,
22
+ listCopy,
23
+ showNotDetected,
24
+ setShowNotDetected,
25
+ p,
26
+ }: {
27
+ connected: AgentInfo[];
28
+ detected: AgentInfo[];
29
+ notFound: AgentInfo[];
30
+ onOpenDetail?: (key: string) => void;
31
+ selectedAgentKey?: string | null;
32
+ mcp: Pick<McpContextValue, 'installAgent'>;
33
+ listCopy: AgentsPanelAgentListRowCopy;
34
+ showNotDetected: boolean;
35
+ setShowNotDetected: (v: boolean | ((prev: boolean) => boolean)) => void;
36
+ p: AgentsCopy;
37
+ }) {
38
+ const open = onOpenDetail ?? (() => {});
39
+
40
+ return (
41
+ <div>
42
+ <div className="px-0 py-1 mb-0.5">
43
+ <span className="text-2xs font-semibold text-muted-foreground uppercase tracking-wider">{p.rosterLabel}</span>
44
+ </div>
45
+ {connected.length > 0 && (
46
+ <section className="mb-3">
47
+ <h3 className="text-[11px] font-medium text-muted-foreground/90 uppercase tracking-wider mb-2 pl-0.5">
48
+ {p.sectionConnected} ({connected.length})
49
+ </h3>
50
+ <div className="space-y-1.5">
51
+ {connected.map(agent => (
52
+ <AgentsPanelAgentListRow
53
+ key={agent.key}
54
+ agent={agent}
55
+ agentStatus="connected"
56
+ selected={selectedAgentKey === agent.key}
57
+ onOpenDetail={() => open(agent.key)}
58
+ onInstallAgent={mcp.installAgent}
59
+ copy={listCopy}
60
+ />
61
+ ))}
62
+ </div>
63
+ </section>
64
+ )}
65
+
66
+ {detected.length > 0 && (
67
+ <section className="mb-3">
68
+ <h3 className="text-[11px] font-medium text-muted-foreground/90 uppercase tracking-wider mb-2 pl-0.5">
69
+ {p.sectionDetected} ({detected.length})
70
+ </h3>
71
+ <div className="space-y-1.5">
72
+ {detected.map(agent => (
73
+ <AgentsPanelAgentListRow
74
+ key={agent.key}
75
+ agent={agent}
76
+ agentStatus="detected"
77
+ selected={selectedAgentKey === agent.key}
78
+ onOpenDetail={() => open(agent.key)}
79
+ onInstallAgent={mcp.installAgent}
80
+ copy={listCopy}
81
+ />
82
+ ))}
83
+ </div>
84
+ </section>
85
+ )}
86
+
87
+ {notFound.length > 0 && (
88
+ <section>
89
+ <button
90
+ type="button"
91
+ onClick={() => setShowNotDetected(!showNotDetected)}
92
+ className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm pl-0.5"
93
+ >
94
+ {showNotDetected ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
95
+ {p.sectionNotDetected} ({notFound.length})
96
+ </button>
97
+ {showNotDetected && (
98
+ <div className="space-y-1.5">
99
+ {notFound.map(agent => (
100
+ <AgentsPanelAgentListRow
101
+ key={agent.key}
102
+ agent={agent}
103
+ agentStatus="notFound"
104
+ selected={selectedAgentKey === agent.key}
105
+ onOpenDetail={() => open(agent.key)}
106
+ onInstallAgent={mcp.installAgent}
107
+ copy={listCopy}
108
+ />
109
+ ))}
110
+ </div>
111
+ )}
112
+ </section>
113
+ )}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { ChevronRight, Loader2 } from 'lucide-react';
5
+ import type { AgentInfo } from '../settings/types';
6
+
7
+ export type AgentsPanelAgentListStatus = 'connected' | 'detected' | 'notFound';
8
+
9
+ export interface AgentsPanelAgentListRowCopy {
10
+ installing: string;
11
+ install: (name: string) => string;
12
+ }
13
+
14
+ export default function AgentsPanelAgentListRow({
15
+ agent,
16
+ agentStatus,
17
+ selected = false,
18
+ onOpenDetail,
19
+ onInstallAgent,
20
+ copy,
21
+ }: {
22
+ agent: AgentInfo;
23
+ agentStatus: AgentsPanelAgentListStatus;
24
+ selected?: boolean;
25
+ onOpenDetail: () => void;
26
+ onInstallAgent: (key: string) => Promise<boolean>;
27
+ copy: AgentsPanelAgentListRowCopy;
28
+ }) {
29
+ const dot =
30
+ agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
31
+
32
+ return (
33
+ <div
34
+ className={`
35
+ group flex items-center gap-0 rounded-xl border transition-all duration-150
36
+ ${selected
37
+ ? 'border-border ring-2 ring-ring/50 bg-[var(--amber-dim)]/45'
38
+ : 'border-border/70 bg-card/50 hover:border-border hover:bg-muted/25'}
39
+ `}
40
+ >
41
+ <button
42
+ type="button"
43
+ onClick={onOpenDetail}
44
+ className="flex flex-1 min-w-0 items-center gap-2.5 text-left rounded-xl pl-3 pr-2 py-2.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
45
+ >
46
+ <span className={`w-2 h-2 rounded-full shrink-0 ring-2 ring-background ${dot}`} />
47
+ <span className="text-sm font-medium text-foreground truncate leading-tight">{agent.name}</span>
48
+ {agentStatus === 'connected' && agent.transport && (
49
+ <span className="text-2xs font-mono tabular-nums px-1.5 py-0.5 rounded-md bg-muted/90 text-muted-foreground shrink-0 border border-border/50">
50
+ {agent.transport}
51
+ </span>
52
+ )}
53
+ <span className="flex-1 min-w-[4px]" />
54
+ <ChevronRight
55
+ size={15}
56
+ className={`shrink-0 transition-opacity duration-150 ${selected ? 'text-[var(--amber)] opacity-90' : 'text-muted-foreground/45 group-hover:text-muted-foreground/80'}`}
57
+ aria-hidden
58
+ />
59
+ </button>
60
+
61
+ {agentStatus === 'detected' && (
62
+ <div className="pr-2 py-2 shrink-0">
63
+ <AgentInstallButton agentKey={agent.key} agentName={agent.name} onInstallAgent={onInstallAgent} copy={copy} />
64
+ </div>
65
+ )}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ function AgentInstallButton({
71
+ agentKey,
72
+ agentName,
73
+ onInstallAgent,
74
+ copy,
75
+ }: {
76
+ agentKey: string;
77
+ agentName: string;
78
+ onInstallAgent: (key: string) => Promise<boolean>;
79
+ copy: AgentsPanelAgentListRowCopy;
80
+ }) {
81
+ const [installing, setInstalling] = useState(false);
82
+
83
+ const handleInstall = async (e: React.MouseEvent) => {
84
+ e.stopPropagation();
85
+ setInstalling(true);
86
+ await onInstallAgent(agentKey);
87
+ setInstalling(false);
88
+ };
89
+
90
+ return (
91
+ <button
92
+ type="button"
93
+ onClick={handleInstall}
94
+ disabled={installing}
95
+ className="flex items-center gap-1 px-2 py-1.5 text-2xs rounded-lg font-medium text-[var(--amber-foreground)] disabled:opacity-50 transition-colors shrink-0 bg-[var(--amber)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
96
+ >
97
+ {installing ? <Loader2 size={10} className="animate-spin" /> : null}
98
+ {installing ? copy.installing : copy.install(agentName)}
99
+ </button>
100
+ );
101
+ }
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { type RefObject } from 'react';
4
+ import { LayoutDashboard, Server, Zap } from 'lucide-react';
5
+ import { PanelNavRow } from './PanelNavRow';
6
+
7
+ type HubCopy = {
8
+ navOverview: string;
9
+ navMcp: string;
10
+ navSkills: string;
11
+ };
12
+
13
+ export function AgentsPanelHubNav({
14
+ copy,
15
+ connectedCount,
16
+ overviewRef,
17
+ skillsRef,
18
+ scrollTo,
19
+ openAdvancedConfig,
20
+ }: {
21
+ copy: HubCopy;
22
+ connectedCount: number;
23
+ overviewRef: RefObject<HTMLDivElement | null>;
24
+ skillsRef: RefObject<HTMLDivElement | null>;
25
+ scrollTo: (el: HTMLElement | null) => void;
26
+ openAdvancedConfig: () => void;
27
+ }) {
28
+ return (
29
+ <div className="py-2">
30
+ <PanelNavRow
31
+ icon={<LayoutDashboard size={14} className="text-[var(--amber)]" />}
32
+ title={copy.navOverview}
33
+ badge={<span className="text-2xs tabular-nums text-muted-foreground">{connectedCount}</span>}
34
+ onClick={() => scrollTo(overviewRef.current)}
35
+ />
36
+ <PanelNavRow
37
+ icon={<Server size={14} className="text-muted-foreground" />}
38
+ title={copy.navMcp}
39
+ onClick={openAdvancedConfig}
40
+ />
41
+ <PanelNavRow
42
+ icon={<Zap size={14} className="text-muted-foreground" />}
43
+ title={copy.navSkills}
44
+ onClick={() => scrollTo(skillsRef.current)}
45
+ />
46
+ </div>
47
+ );
48
+ }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import Link from 'next/link';
4
- import { Lightbulb, Blocks, Zap, LayoutTemplate, ChevronRight, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck } from 'lucide-react';
3
+ import { Lightbulb, Blocks, Zap, LayoutTemplate, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck } from 'lucide-react';
5
4
  import PanelHeader from './PanelHeader';
5
+ import { PanelNavRow, ComingSoonBadge } from './PanelNavRow';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { useCases } from '@/components/explore/use-cases';
8
8
  import { openAskModal } from '@/hooks/useAskModal';
@@ -13,46 +13,6 @@ interface DiscoverPanelProps {
13
13
  onMaximize?: () => void;
14
14
  }
15
15
 
16
- /** Navigation entry — clickable row linking to a page or showing coming soon */
17
- function NavEntry({
18
- icon,
19
- title,
20
- badge,
21
- href,
22
- onClick,
23
- }: {
24
- icon: React.ReactNode;
25
- title: string;
26
- badge?: React.ReactNode;
27
- href?: string;
28
- onClick?: () => void;
29
- }) {
30
- const content = (
31
- <>
32
- <span className="flex items-center justify-center w-7 h-7 rounded-md bg-muted shrink-0">{icon}</span>
33
- <span className="text-sm font-medium text-foreground flex-1">{title}</span>
34
- {badge}
35
- <ChevronRight size={14} className="text-muted-foreground shrink-0" />
36
- </>
37
- );
38
-
39
- const className = "flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50 transition-colors cursor-pointer";
40
-
41
- if (href) {
42
- return <Link href={href} className={className}>{content}</Link>;
43
- }
44
- return <button onClick={onClick} className={`${className} w-full`}>{content}</button>;
45
- }
46
-
47
- /** Coming soon badge */
48
- function ComingSoonBadge({ label }: { label: string }) {
49
- return (
50
- <span className="text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground shrink-0">
51
- {label}
52
- </span>
53
- );
54
- }
55
-
56
16
  /** Compact use case row */
57
17
  function UseCaseRow({
58
18
  icon,
@@ -112,23 +72,23 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
112
72
  <div className="flex-1 overflow-y-auto min-h-0">
113
73
  {/* Navigation entries */}
114
74
  <div className="py-2">
115
- <NavEntry
75
+ <PanelNavRow
116
76
  icon={<Lightbulb size={14} className="text-[var(--amber)]" />}
117
77
  title={d.useCases}
118
78
  badge={<span className="text-2xs tabular-nums text-muted-foreground">{useCases.length}</span>}
119
79
  href="/explore"
120
80
  />
121
- <NavEntry
81
+ <PanelNavRow
122
82
  icon={<Blocks size={14} className="text-muted-foreground" />}
123
83
  title={d.pluginMarket}
124
84
  badge={<ComingSoonBadge label={d.comingSoon} />}
125
85
  />
126
- <NavEntry
86
+ <PanelNavRow
127
87
  icon={<Zap size={14} className="text-muted-foreground" />}
128
88
  title={d.skillMarket}
129
89
  badge={<ComingSoonBadge label={d.comingSoon} />}
130
90
  />
131
- <NavEntry
91
+ <PanelNavRow
132
92
  icon={<LayoutTemplate size={14} className="text-muted-foreground" />}
133
93
  title={d.spaceTemplates}
134
94
  badge={<ComingSoonBadge label={d.comingSoon} />}
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import { UserRound, Bookmark, Sun, History, Brain } from 'lucide-react';
6
+ import PanelHeader from './PanelHeader';
7
+ import { PanelNavRow } from './PanelNavRow';
8
+ import { useLocale } from '@/lib/LocaleContext';
9
+ import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
10
+
11
+ interface EchoPanelProps {
12
+ active: boolean;
13
+ maximized?: boolean;
14
+ onMaximize?: () => void;
15
+ }
16
+
17
+ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelProps) {
18
+ const { t } = useLocale();
19
+ const e = t.panels.echo;
20
+ const pathname = usePathname() ?? '';
21
+
22
+ const rowBySegment: Record<EchoSegment, { icon: ReactNode; title: string }> = {
23
+ 'about-you': { icon: <UserRound size={14} />, title: e.aboutYouTitle },
24
+ continued: { icon: <Bookmark size={14} />, title: e.continuedTitle },
25
+ daily: { icon: <Sun size={14} />, title: e.dailyEchoTitle },
26
+ 'past-you': { icon: <History size={14} />, title: e.pastYouTitle },
27
+ growth: { icon: <Brain size={14} />, title: e.intentGrowthTitle },
28
+ };
29
+
30
+ return (
31
+ <div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
32
+ <PanelHeader title={e.title} maximized={maximized} onMaximize={onMaximize} />
33
+ <div className="flex-1 overflow-y-auto min-h-0">
34
+ <div className="flex flex-col py-1">
35
+ {ECHO_SEGMENT_ORDER.map((segment) => {
36
+ const row = rowBySegment[segment];
37
+ const href = ECHO_SEGMENT_HREF[segment];
38
+ const isActive = pathname === href || pathname.startsWith(`${href}/`);
39
+ return (
40
+ <div key={segment} className="border-b border-border/60 last:border-b-0">
41
+ <PanelNavRow href={href} icon={row.icon} title={row.title} active={isActive} />
42
+ </div>
43
+ );
44
+ })}
45
+ </div>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import Link from 'next/link';
5
+ import { ChevronRight } from 'lucide-react';
6
+ import { cn } from '@/lib/utils';
7
+
8
+ /** Row matching Discover panel nav: icon tile, title, optional badge, chevron. */
9
+ export function PanelNavRow({
10
+ icon,
11
+ title,
12
+ badge,
13
+ href,
14
+ onClick,
15
+ active,
16
+ }: {
17
+ icon: ReactNode;
18
+ title: string;
19
+ badge?: React.ReactNode;
20
+ href?: string;
21
+ onClick?: () => void;
22
+ /** When true, row shows selected state (e.g. current Echo segment). */
23
+ active?: boolean;
24
+ }) {
25
+ const content = (
26
+ <>
27
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted">{icon}</span>
28
+ <span className="flex-1 text-left text-sm font-medium text-foreground">{title}</span>
29
+ {badge}
30
+ <ChevronRight size={14} className="shrink-0 text-muted-foreground" />
31
+ </>
32
+ );
33
+
34
+ const showRail = Boolean(active && href);
35
+
36
+ const className = cn(
37
+ 'relative flex items-center gap-3 py-2.5 transition-colors duration-150 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
38
+ showRail ? 'bg-[var(--amber-dim)]/40 pl-3.5 pr-4 text-foreground' : 'px-4',
39
+ href && !showRail && 'cursor-pointer hover:bg-muted/50',
40
+ showRail && 'cursor-default',
41
+ !href && 'cursor-pointer hover:bg-muted/50',
42
+ );
43
+
44
+ if (href) {
45
+ return (
46
+ <Link href={href} className={className} aria-current={active ? 'page' : undefined}>
47
+ {showRail ? (
48
+ <span
49
+ className="pointer-events-none absolute bottom-[22%] left-0 top-[22%] w-0.5 rounded-r-full bg-[var(--amber)]"
50
+ aria-hidden
51
+ />
52
+ ) : null}
53
+ {content}
54
+ </Link>
55
+ );
56
+ }
57
+ return (
58
+ <button type="button" onClick={onClick} className={cn(className, 'w-full')}>
59
+ {content}
60
+ </button>
61
+ );
62
+ }
63
+
64
+ export function ComingSoonBadge({ label }: { label: string }) {
65
+ return (
66
+ <span className="text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground shrink-0">{label}</span>
67
+ );
68
+ }
@@ -0,0 +1,13 @@
1
+ export type AgentsPanelAgentDetailStatus = 'connected' | 'detected' | 'notFound';
2
+
3
+ export function resolveAgentDetailStatus(
4
+ key: string,
5
+ connected: { key: string }[],
6
+ detected: { key: string }[],
7
+ notFound: { key: string }[],
8
+ ): AgentsPanelAgentDetailStatus | null {
9
+ if (connected.some(a => a.key === key)) return 'connected';
10
+ if (detected.some(a => a.key === key)) return 'detected';
11
+ if (notFound.some(a => a.key === key)) return 'notFound';
12
+ return null;
13
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { isAiConfiguredForAsk, type SettingsJsonForAi } from '@/lib/settings-ai-client';
5
+
6
+ export function useSettingsAiAvailable(): { ready: boolean; loading: boolean } {
7
+ const [ready, setReady] = useState(false);
8
+ const [loading, setLoading] = useState(true);
9
+
10
+ useEffect(() => {
11
+ let cancelled = false;
12
+ fetch('/api/settings', { cache: 'no-store' })
13
+ .then((r) => r.json())
14
+ .then((d: SettingsJsonForAi) => {
15
+ if (!cancelled) setReady(isAiConfiguredForAsk(d));
16
+ })
17
+ .catch(() => {
18
+ if (!cancelled) setReady(false);
19
+ })
20
+ .finally(() => {
21
+ if (!cancelled) setLoading(false);
22
+ });
23
+ return () => {
24
+ cancelled = true;
25
+ };
26
+ }, []);
27
+
28
+ return { ready, loading };
29
+ }
@@ -0,0 +1,44 @@
1
+ import type { EchoSegment } from '@/lib/echo-segments';
2
+
3
+ export function buildEchoInsightUserPrompt(opts: {
4
+ locale: 'en' | 'zh';
5
+ segment: EchoSegment;
6
+ segmentTitle: string;
7
+ factsHeading: string;
8
+ emptyTitle: string;
9
+ emptyBody: string;
10
+ continuedDrafts: string;
11
+ continuedTodos: string;
12
+ subEmptyHint: string;
13
+ dailyLineLabel: string;
14
+ dailyLine: string;
15
+ growthIntentLabel: string;
16
+ growthIntent: string;
17
+ }): string {
18
+ const lang = opts.locale === 'zh' ? 'Chinese' : 'English';
19
+ const lines: string[] = [
20
+ `Echo section: ${opts.segmentTitle}`,
21
+ `${opts.factsHeading}: ${opts.emptyTitle}`,
22
+ opts.emptyBody,
23
+ ];
24
+
25
+ if (opts.segment === 'continued') {
26
+ lines.push(`${opts.continuedDrafts} — ${opts.subEmptyHint}`, `${opts.continuedTodos} — ${opts.subEmptyHint}`);
27
+ }
28
+ if (opts.segment === 'daily' && opts.dailyLine.trim()) {
29
+ lines.push(`${opts.dailyLineLabel}: ${opts.dailyLine.trim()}`);
30
+ }
31
+ if (opts.segment === 'growth' && opts.growthIntent.trim()) {
32
+ lines.push(`${opts.growthIntentLabel}: ${opts.growthIntent.trim()}`);
33
+ }
34
+
35
+ const context = lines.join('\n\n');
36
+
37
+ return `You are a reflective assistant inside MindOS Echo (a personal, local-first notes companion). The user is viewing one Echo section. Below is exactly what they see on screen right now—it may be an empty-state placeholder until indexing fills the list.
38
+
39
+ --- Visible context ---
40
+ ${context}
41
+ ---
42
+
43
+ Write a short insight in ${lang} as Markdown (under 220 words). Tone: warm, restrained, "quiet notebook"—not a hype coach. If the context is only generic empty copy, acknowledge that briefly and offer 2–3 reflection prompts instead of inventing files or facts. Do not claim you read their whole library. Prefer answering from this context; avoid unnecessary tool use.`;
44
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Echo content routes: segment slugs and validation.
3
+ * Keep in sync with `wiki/specs/spec-echo-content-pages.md`.
4
+ */
5
+
6
+ export const ECHO_SEGMENT_IDS = ['about-you', 'continued', 'daily', 'past-you', 'growth'] as const;
7
+
8
+ export type EchoSegment = (typeof ECHO_SEGMENT_IDS)[number];
9
+
10
+ export const ECHO_SEGMENT_ORDER: readonly EchoSegment[] = ECHO_SEGMENT_IDS;
11
+
12
+ /** App Router paths for each segment (single source for panel + in-page nav). */
13
+ export const ECHO_SEGMENT_HREF: Record<EchoSegment, string> = {
14
+ 'about-you': '/echo/about-you',
15
+ continued: '/echo/continued',
16
+ daily: '/echo/daily',
17
+ 'past-you': '/echo/past-you',
18
+ growth: '/echo/growth',
19
+ };
20
+
21
+ export function isEchoSegment(value: string): value is EchoSegment {
22
+ return (ECHO_SEGMENT_IDS as readonly string[]).includes(value);
23
+ }
24
+
25
+ export function defaultEchoSegment(): EchoSegment {
26
+ return 'about-you';
27
+ }