@geminilight/mindos 0.5.62 → 0.5.64

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 (41) hide show
  1. package/app/app/api/changes/route.ts +7 -1
  2. package/app/app/api/mcp/install-skill/route.ts +9 -24
  3. package/app/app/api/mcp/status/route.ts +1 -1
  4. package/app/app/layout.tsx +1 -0
  5. package/app/app/page.tsx +1 -2
  6. package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
  7. package/app/components/HomeContent.tsx +41 -6
  8. package/app/components/RightAgentDetailPanel.tsx +1 -0
  9. package/app/components/SidebarLayout.tsx +1 -0
  10. package/app/components/agents/AgentsContentPage.tsx +20 -16
  11. package/app/components/agents/AgentsMcpSection.tsx +178 -65
  12. package/app/components/agents/AgentsOverviewSection.tsx +1 -1
  13. package/app/components/agents/AgentsSkillsSection.tsx +78 -55
  14. package/app/components/agents/agents-content-model.ts +16 -0
  15. package/app/components/changes/ChangesBanner.tsx +90 -13
  16. package/app/components/changes/ChangesContentPage.tsx +134 -51
  17. package/app/components/panels/AgentsPanel.tsx +14 -28
  18. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -4
  19. package/app/components/panels/AgentsPanelAgentGroups.tsx +5 -6
  20. package/app/components/panels/AgentsPanelAgentListRow.tsx +30 -5
  21. package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
  22. package/app/components/panels/PluginsPanel.tsx +3 -3
  23. package/app/components/renderers/agent-inspector/manifest.ts +2 -0
  24. package/app/components/renderers/config/manifest.ts +1 -0
  25. package/app/components/renderers/csv/manifest.ts +1 -0
  26. package/app/components/settings/PluginsTab.tsx +4 -3
  27. package/app/hooks/useMcpData.tsx +3 -2
  28. package/app/lib/core/content-changes.ts +148 -8
  29. package/app/lib/fs.ts +7 -1
  30. package/app/lib/i18n-en.ts +58 -3
  31. package/app/lib/i18n-zh.ts +58 -3
  32. package/app/lib/mcp-agents.ts +42 -0
  33. package/app/lib/renderers/registry.ts +10 -0
  34. package/app/next-env.d.ts +1 -1
  35. package/bin/lib/mcp-agents.js +38 -13
  36. package/package.json +1 -1
  37. package/scripts/migrate-agent-diff.js +146 -0
  38. package/scripts/setup.js +12 -17
  39. package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
  40. package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
  41. package/app/components/renderers/diff/manifest.ts +0 -14
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useRef, useCallback } from 'react';
4
- import { useRouter } from 'next/navigation';
3
+ import { useState } from 'react';
5
4
  import { Loader2, RefreshCw, ChevronDown, ChevronRight, Settings } from 'lucide-react';
6
5
  import { useMcpData } from '@/hooks/useMcpData';
7
6
  import { useLocale } from '@/lib/LocaleContext';
@@ -28,26 +27,18 @@ export default function AgentsPanel({
28
27
  onOpenAgentDetail,
29
28
  }: AgentsPanelProps) {
30
29
  const { t } = useLocale();
31
- const router = useRouter();
32
30
  const p = t.panels.agents;
33
31
  const mcp = useMcpData();
34
32
  const [refreshing, setRefreshing] = useState(false);
35
33
  const [showNotDetected, setShowNotDetected] = useState(false);
36
34
  const [showBuiltinSkills, setShowBuiltinSkills] = useState(false);
37
35
 
38
- const overviewRef = useRef<HTMLDivElement>(null);
39
- const skillsRef = useRef<HTMLDivElement>(null);
40
-
41
36
  const handleRefresh = async () => {
42
37
  setRefreshing(true);
43
38
  await mcp.refresh();
44
39
  setRefreshing(false);
45
40
  };
46
41
 
47
- const scrollTo = useCallback((el: HTMLElement | null) => {
48
- el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
49
- }, []);
50
-
51
42
  const openAdvancedConfig = () => {
52
43
  window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
53
44
  };
@@ -59,10 +50,18 @@ export default function AgentsPanel({
59
50
  const customSkills = mcp.skills.filter(s => s.source === 'user');
60
51
  const builtinSkills = mcp.skills.filter(s => s.source === 'builtin');
61
52
  const activeSkillCount = mcp.skills.filter(s => s.enabled).length;
53
+ const installAgentWithRefresh = async (key: string) => {
54
+ const ok = await mcp.installAgent(key);
55
+ if (ok) await mcp.refresh();
56
+ return ok;
57
+ };
62
58
 
63
59
  const listCopy = {
64
60
  installing: p.installing,
65
61
  install: p.install,
62
+ installSuccess: p.installSuccess,
63
+ installFailed: p.installFailed,
64
+ retryInstall: p.retryInstall,
66
65
  };
67
66
 
68
67
  const hubCopy = {
@@ -75,10 +74,6 @@ export default function AgentsPanel({
75
74
  <AgentsPanelHubNav
76
75
  copy={hubCopy}
77
76
  connectedCount={connected.length}
78
- overviewRef={overviewRef}
79
- skillsRef={skillsRef}
80
- scrollTo={scrollTo}
81
- openAdvancedConfig={openAdvancedConfig}
82
77
  />
83
78
  );
84
79
 
@@ -91,15 +86,6 @@ export default function AgentsPanel({
91
86
  {connected.length} {p.connected}
92
87
  </span>
93
88
  )}
94
- <button
95
- onClick={() => router.push('/agents')}
96
- className="px-2 py-1 rounded border border-border text-2xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
97
- aria-label={p.openDashboard}
98
- title={p.openDashboard}
99
- type="button"
100
- >
101
- {p.openDashboard}
102
- </button>
103
89
  <button
104
90
  onClick={handleRefresh}
105
91
  disabled={refreshing}
@@ -122,7 +108,7 @@ export default function AgentsPanel({
122
108
  <div className="flex flex-col gap-2 py-4 px-0">
123
109
  {hub}
124
110
  <div className="mx-4 border-t border-border" />
125
- <div ref={overviewRef} className="mx-3 rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between scroll-mt-2">
111
+ <div className="mx-3 rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between">
126
112
  <span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
127
113
  {mcp.status?.running ? (
128
114
  <span className="flex items-center gap-1.5 text-[11px]">
@@ -137,7 +123,7 @@ export default function AgentsPanel({
137
123
  </span>
138
124
  )}
139
125
  </div>
140
- <div ref={skillsRef} className="mx-3 scroll-mt-2 rounded-lg border border-dashed border-border px-3 py-3 text-center">
126
+ <div className="mx-3 rounded-lg border border-dashed border-border px-3 py-3 text-center">
141
127
  <p className="text-xs text-muted-foreground mb-2">{p.noAgents}</p>
142
128
  <p className="text-2xs text-muted-foreground mb-3">{p.skillsEmptyHint}</p>
143
129
  <button
@@ -156,7 +142,7 @@ export default function AgentsPanel({
156
142
  <div className="mx-4 border-t border-border" />
157
143
 
158
144
  <div className="px-3 py-3 space-y-4">
159
- <div ref={overviewRef} className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between scroll-mt-2">
145
+ <div className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between">
160
146
  <span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
161
147
  {mcp.status?.running ? (
162
148
  <span className="flex items-center gap-1.5 text-[11px]">
@@ -178,8 +164,8 @@ export default function AgentsPanel({
178
164
  notFound={notFound}
179
165
  onOpenDetail={onOpenAgentDetail}
180
166
  selectedAgentKey={selectedAgentKey}
181
- mcp={mcp}
182
167
  listCopy={listCopy}
168
+ onInstallAgent={installAgentWithRefresh}
183
169
  showNotDetected={showNotDetected}
184
170
  setShowNotDetected={setShowNotDetected}
185
171
  p={{
@@ -190,7 +176,7 @@ export default function AgentsPanel({
190
176
  }}
191
177
  />
192
178
 
193
- <section ref={skillsRef} className="scroll-mt-2">
179
+ <section>
194
180
  {mcp.skills.length > 0 ? (
195
181
  <>
196
182
  <div className="flex items-center justify-between mb-2">
@@ -12,7 +12,8 @@ export type { AgentsPanelAgentDetailStatus };
12
12
  export interface AgentsPanelAgentDetailCopy {
13
13
  connected: string;
14
14
  installing: string;
15
- install: (name: string) => string;
15
+ install: string;
16
+ installFailed: string;
16
17
  copyConfig: string;
17
18
  copied: string;
18
19
  transportLocal: string;
@@ -61,7 +62,7 @@ export default function AgentsPanelAgentDetail({
61
62
  setResult(
62
63
  ok
63
64
  ? { type: 'success', text: `${agent.name} ${copy.connected}` }
64
- : { type: 'error', text: 'Install failed' },
65
+ : { type: 'error', text: copy.installFailed },
65
66
  );
66
67
  setInstalling(false);
67
68
  };
@@ -117,10 +118,10 @@ export default function AgentsPanelAgentDetail({
117
118
  type="button"
118
119
  onClick={handleInstall}
119
120
  disabled={installing}
120
- className="inline-flex items-center gap-1.5 px-3 py-2 text-xs rounded-lg font-medium text-[var(--amber-foreground)] disabled:opacity-50 bg-[var(--amber)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
121
+ className="inline-flex items-center gap-1.5 px-3 py-2 text-xs rounded-lg font-medium text-white disabled:opacity-50 bg-[var(--amber)] hover:bg-[var(--amber)]/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
121
122
  >
122
123
  {installing ? <Loader2 size={14} className="animate-spin" /> : null}
123
- {installing ? copy.installing : copy.install(agent.name)}
124
+ {installing ? copy.installing : copy.install}
124
125
  </button>
125
126
  {result && (
126
127
  <span
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { ChevronDown, ChevronRight } from 'lucide-react';
4
4
  import type { AgentInfo } from '../settings/types';
5
- import type { McpContextValue } from '@/hooks/useMcpData';
6
5
  import AgentsPanelAgentListRow, { type AgentsPanelAgentListRowCopy } from './AgentsPanelAgentListRow';
7
6
 
8
7
  type AgentsCopy = {
@@ -18,7 +17,7 @@ export function AgentsPanelAgentGroups({
18
17
  notFound,
19
18
  onOpenDetail,
20
19
  selectedAgentKey,
21
- mcp,
20
+ onInstallAgent,
22
21
  listCopy,
23
22
  showNotDetected,
24
23
  setShowNotDetected,
@@ -29,7 +28,7 @@ export function AgentsPanelAgentGroups({
29
28
  notFound: AgentInfo[];
30
29
  onOpenDetail?: (key: string) => void;
31
30
  selectedAgentKey?: string | null;
32
- mcp: Pick<McpContextValue, 'installAgent'>;
31
+ onInstallAgent: (key: string) => Promise<boolean>;
33
32
  listCopy: AgentsPanelAgentListRowCopy;
34
33
  showNotDetected: boolean;
35
34
  setShowNotDetected: (v: boolean | ((prev: boolean) => boolean)) => void;
@@ -55,7 +54,7 @@ export function AgentsPanelAgentGroups({
55
54
  agentStatus="connected"
56
55
  selected={selectedAgentKey === agent.key}
57
56
  onOpenDetail={() => open(agent.key)}
58
- onInstallAgent={mcp.installAgent}
57
+ onInstallAgent={onInstallAgent}
59
58
  copy={listCopy}
60
59
  />
61
60
  ))}
@@ -76,7 +75,7 @@ export function AgentsPanelAgentGroups({
76
75
  agentStatus="detected"
77
76
  selected={selectedAgentKey === agent.key}
78
77
  onOpenDetail={() => open(agent.key)}
79
- onInstallAgent={mcp.installAgent}
78
+ onInstallAgent={onInstallAgent}
80
79
  copy={listCopy}
81
80
  />
82
81
  ))}
@@ -103,7 +102,7 @@ export function AgentsPanelAgentGroups({
103
102
  agentStatus="notFound"
104
103
  selected={selectedAgentKey === agent.key}
105
104
  onOpenDetail={() => open(agent.key)}
106
- onInstallAgent={mcp.installAgent}
105
+ onInstallAgent={onInstallAgent}
107
106
  copy={listCopy}
108
107
  />
109
108
  ))}
@@ -1,14 +1,17 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import { ChevronRight, Loader2 } from 'lucide-react';
4
+ import { Check, ChevronRight, Loader2, RotateCw } from 'lucide-react';
5
5
  import type { AgentInfo } from '../settings/types';
6
6
 
7
7
  export type AgentsPanelAgentListStatus = 'connected' | 'detected' | 'notFound';
8
8
 
9
9
  export interface AgentsPanelAgentListRowCopy {
10
10
  installing: string;
11
- install: (name: string) => string;
11
+ install: string;
12
+ installSuccess: string;
13
+ installFailed: string;
14
+ retryInstall: string;
12
15
  }
13
16
 
14
17
  export default function AgentsPanelAgentListRow({
@@ -79,23 +82,45 @@ function AgentInstallButton({
79
82
  copy: AgentsPanelAgentListRowCopy;
80
83
  }) {
81
84
  const [installing, setInstalling] = useState(false);
85
+ const [installState, setInstallState] = useState<'idle' | 'success' | 'error'>('idle');
82
86
 
83
87
  const handleInstall = async (e: React.MouseEvent) => {
84
88
  e.stopPropagation();
89
+ if (installing) return;
85
90
  setInstalling(true);
86
- await onInstallAgent(agentKey);
91
+ setInstallState('idle');
92
+ const ok = await onInstallAgent(agentKey);
87
93
  setInstalling(false);
94
+ setInstallState(ok ? 'success' : 'error');
88
95
  };
89
96
 
97
+ const isError = installState === 'error';
98
+ const isSuccess = installState === 'success';
99
+ const label = installing
100
+ ? copy.installing
101
+ : isSuccess
102
+ ? copy.installSuccess
103
+ : isError
104
+ ? copy.retryInstall
105
+ : copy.install;
106
+
90
107
  return (
91
108
  <button
92
109
  type="button"
93
110
  onClick={handleInstall}
94
111
  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"
112
+ className={`flex items-center gap-1 px-2 py-1.5 text-2xs rounded-lg font-medium text-white disabled:opacity-50 transition-colors shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
113
+ isError ? 'bg-error hover:bg-error/90' : isSuccess ? 'bg-success hover:bg-success/90' : 'bg-[var(--amber)] hover:bg-[var(--amber)]/90'
114
+ }`}
115
+ aria-label={`${agentName} ${label}`}
96
116
  >
97
117
  {installing ? <Loader2 size={10} className="animate-spin" /> : null}
98
- {installing ? copy.installing : copy.install(agentName)}
118
+ {!installing && isSuccess ? <Check size={10} /> : null}
119
+ {!installing && isError ? <RotateCw size={10} /> : null}
120
+ {label}
121
+ <span className="sr-only" aria-live="polite">
122
+ {isError ? copy.installFailed : ''}
123
+ </span>
99
124
  </button>
100
125
  );
101
126
  }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { type RefObject } from 'react';
3
+ import { usePathname, useSearchParams } from 'next/navigation';
4
4
  import { LayoutDashboard, Server, Zap } from 'lucide-react';
5
5
  import { PanelNavRow } from './PanelNavRow';
6
6
 
@@ -13,35 +13,35 @@ type HubCopy = {
13
13
  export function AgentsPanelHubNav({
14
14
  copy,
15
15
  connectedCount,
16
- overviewRef,
17
- skillsRef,
18
- scrollTo,
19
- openAdvancedConfig,
20
16
  }: {
21
17
  copy: HubCopy;
22
18
  connectedCount: number;
23
- overviewRef: RefObject<HTMLDivElement | null>;
24
- skillsRef: RefObject<HTMLDivElement | null>;
25
- scrollTo: (el: HTMLElement | null) => void;
26
- openAdvancedConfig: () => void;
27
19
  }) {
20
+ const pathname = usePathname();
21
+ const searchParams = useSearchParams();
22
+ const tab = searchParams.get('tab');
23
+ const inAgentsRoute = pathname === '/agents';
24
+
28
25
  return (
29
26
  <div className="py-2">
30
27
  <PanelNavRow
31
28
  icon={<LayoutDashboard size={14} className="text-[var(--amber)]" />}
32
29
  title={copy.navOverview}
33
30
  badge={<span className="text-2xs tabular-nums text-muted-foreground">{connectedCount}</span>}
34
- onClick={() => scrollTo(overviewRef.current)}
31
+ href="/agents"
32
+ active={inAgentsRoute && (tab === null || tab === 'overview')}
35
33
  />
36
34
  <PanelNavRow
37
35
  icon={<Server size={14} className="text-muted-foreground" />}
38
36
  title={copy.navMcp}
39
- onClick={openAdvancedConfig}
37
+ href="/agents?tab=mcp"
38
+ active={inAgentsRoute && tab === 'mcp'}
40
39
  />
41
40
  <PanelNavRow
42
41
  icon={<Zap size={14} className="text-muted-foreground" />}
43
42
  title={copy.navSkills}
44
- onClick={() => scrollTo(skillsRef.current)}
43
+ href="/agents?tab=skills"
44
+ active={inAgentsRoute && tab === 'skills'}
45
45
  />
46
46
  </div>
47
47
  );
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
- import { getAllRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
5
+ import { getPluginRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
6
6
  import { Toggle } from '../settings/Primitives';
7
7
  import PanelHeader from './PanelHeader';
8
8
  import { useLocale } from '@/lib/LocaleContext';
@@ -32,7 +32,7 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
32
32
  useEffect(() => {
33
33
  if (!mounted || fetchedRef.current) return;
34
34
  fetchedRef.current = true;
35
- const entryPaths = getAllRenderers()
35
+ const entryPaths = getPluginRenderers()
36
36
  .map(r => r.entryPath)
37
37
  .filter((p): p is string => !!p);
38
38
  if (entryPaths.length === 0) return;
@@ -47,7 +47,7 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
47
47
  .catch(() => {});
48
48
  }, [mounted]);
49
49
 
50
- const renderers = mounted ? getAllRenderers() : [];
50
+ const renderers = mounted ? getPluginRenderers() : [];
51
51
  const enabledCount = mounted ? renderers.filter(r => isRendererEnabled(r.id)).length : 0;
52
52
 
53
53
  const handleToggle = useCallback((id: string, enabled: boolean) => {
@@ -8,6 +8,8 @@ export const manifest: RendererDefinition = {
8
8
  icon: '🔍',
9
9
  tags: ['agent', 'inspector', 'log', 'mcp', 'tools'],
10
10
  builtin: true,
11
+ core: true,
12
+ appBuiltinFeature: true,
11
13
  entryPath: '.agent-log.json',
12
14
  match: ({ filePath }) => /\.agent-log\.json$/i.test(filePath),
13
15
  load: () => import('./AgentInspectorRenderer').then(m => ({ default: m.AgentInspectorRenderer })),
@@ -9,6 +9,7 @@ export const manifest: RendererDefinition = {
9
9
  tags: ['config', 'json', 'settings', 'schema'],
10
10
  builtin: true,
11
11
  core: true,
12
+ appBuiltinFeature: true,
12
13
  entryPath: 'CONFIG.json',
13
14
  match: ({ filePath, extension }) => extension === 'json' && /(^|\/)CONFIG\.json$/i.test(filePath),
14
15
  load: () => import('./ConfigRenderer').then(m => ({ default: m.ConfigRenderer })),
@@ -9,6 +9,7 @@ export const manifest: RendererDefinition = {
9
9
  tags: ['csv', 'table', 'gallery', 'board', 'data'],
10
10
  builtin: true,
11
11
  core: true,
12
+ appBuiltinFeature: true,
12
13
  entryPath: 'Resources/Products.csv',
13
14
  match: ({ extension, filePath }) => extension === 'csv' && !/\bTODO\b/i.test(filePath),
14
15
  load: () => import('./CsvRenderer').then(m => ({ default: m.CsvRenderer })),
@@ -1,20 +1,21 @@
1
1
  'use client';
2
2
 
3
3
  import { Puzzle } from 'lucide-react';
4
- import { getAllRenderers, setRendererEnabled } from '@/lib/renderers/registry';
4
+ import { getPluginRenderers, setRendererEnabled } from '@/lib/renderers/registry';
5
5
  import { Toggle } from './Primitives';
6
6
  import type { PluginsTabProps } from './types';
7
7
 
8
8
  export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps) {
9
+ const renderers = getPluginRenderers();
9
10
  return (
10
11
  <div className="space-y-5">
11
12
  <p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{t.settings.plugins.title}</p>
12
13
 
13
- {getAllRenderers().length === 0 ? (
14
+ {renderers.length === 0 ? (
14
15
  <p className="text-sm text-muted-foreground">{t.settings.plugins.noPlugins}</p>
15
16
  ) : (
16
17
  <div className="flex flex-col gap-3">
17
- {getAllRenderers().map(renderer => {
18
+ {renderers.map(renderer => {
18
19
  const isCore = !!renderer.core;
19
20
  const enabled = isCore ? true : (pluginStates[renderer.id] ?? true);
20
21
  return (
@@ -128,7 +128,7 @@ export default function McpProvider({ children }: { children: ReactNode }) {
128
128
  if (!agent) return false;
129
129
 
130
130
  try {
131
- const res = await apiFetch<{ results: Array<{ key: string; ok: boolean; error?: string }> }>('/api/mcp/install', {
131
+ const res = await apiFetch<{ results: Array<{ agent?: string; status?: string; ok?: boolean; error?: string }> }>('/api/mcp/install', {
132
132
  method: 'POST',
133
133
  headers: { 'Content-Type': 'application/json' },
134
134
  body: JSON.stringify({
@@ -141,7 +141,8 @@ export default function McpProvider({ children }: { children: ReactNode }) {
141
141
  }),
142
142
  });
143
143
 
144
- const ok = res.results?.[0]?.ok ?? false;
144
+ const first = res.results?.[0];
145
+ const ok = first?.ok === true || first?.status === 'ok';
145
146
  if (ok) {
146
147
  // Refresh to pick up newly installed agent
147
148
  await fetchAll();
@@ -32,11 +32,18 @@ interface ChangeLogState {
32
32
  version: 1;
33
33
  lastSeenAt: string | null;
34
34
  events: ContentChangeEvent[];
35
+ legacy?: {
36
+ agentDiffImportedCount?: number;
37
+ lastImportedAt?: string | null;
38
+ };
35
39
  }
36
40
 
37
41
  interface ListOptions {
38
42
  path?: string;
39
43
  limit?: number;
44
+ source?: ContentChangeSource;
45
+ op?: string;
46
+ q?: string;
40
47
  }
41
48
 
42
49
  export interface ContentChangeSummary {
@@ -64,6 +71,10 @@ function defaultState(): ChangeLogState {
64
71
  version: 1,
65
72
  lastSeenAt: null,
66
73
  events: [],
74
+ legacy: {
75
+ agentDiffImportedCount: 0,
76
+ lastImportedAt: null,
77
+ },
67
78
  };
68
79
  }
69
80
 
@@ -87,6 +98,16 @@ function readState(mindRoot: string): ChangeLogState {
87
98
  version: 1,
88
99
  lastSeenAt: typeof parsed.lastSeenAt === 'string' ? parsed.lastSeenAt : null,
89
100
  events: parsed.events,
101
+ legacy: {
102
+ agentDiffImportedCount:
103
+ typeof parsed.legacy?.agentDiffImportedCount === 'number'
104
+ ? parsed.legacy.agentDiffImportedCount
105
+ : 0,
106
+ lastImportedAt:
107
+ typeof parsed.legacy?.lastImportedAt === 'string'
108
+ ? parsed.legacy.lastImportedAt
109
+ : null,
110
+ },
90
111
  };
91
112
  } catch {
92
113
  return defaultState();
@@ -99,8 +120,115 @@ function writeState(mindRoot: string, state: ChangeLogState): void {
99
120
  fs.writeFileSync(file, JSON.stringify(state, null, 2), 'utf-8');
100
121
  }
101
122
 
102
- export function appendContentChange(mindRoot: string, input: ContentChangeInput): ContentChangeEvent {
123
+ interface LegacyAgentDiffEntry {
124
+ ts?: string;
125
+ path?: string;
126
+ tool?: string;
127
+ before?: string;
128
+ after?: string;
129
+ }
130
+
131
+ function parseLegacyAgentDiffBlocks(content: string): LegacyAgentDiffEntry[] {
132
+ const blocks: LegacyAgentDiffEntry[] = [];
133
+ const re = /```agent-diff\s*\n([\s\S]*?)```/g;
134
+ let m: RegExpExecArray | null;
135
+ while ((m = re.exec(content)) !== null) {
136
+ try {
137
+ const parsed = JSON.parse(m[1].trim()) as LegacyAgentDiffEntry;
138
+ blocks.push(parsed);
139
+ } catch {
140
+ // Skip malformed block, keep import best-effort.
141
+ }
142
+ }
143
+ return blocks;
144
+ }
145
+
146
+ function toValidIso(ts: string | undefined): string {
147
+ if (!ts) return nowIso();
148
+ const ms = new Date(ts).getTime();
149
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : nowIso();
150
+ }
151
+
152
+ function removeLegacyFile(filePath: string): void {
153
+ try {
154
+ fs.rmSync(filePath, { force: true });
155
+ } catch {
156
+ // keep best-effort; migration should not fail main flow.
157
+ }
158
+ }
159
+
160
+ function importLegacyAgentDiffIfNeeded(mindRoot: string, state: ChangeLogState): ChangeLogState {
161
+ const legacyPath = path.join(mindRoot, 'Agent-Diff.md');
162
+ if (!fs.existsSync(legacyPath)) return state;
163
+
164
+ let raw = '';
165
+ try {
166
+ raw = fs.readFileSync(legacyPath, 'utf-8');
167
+ } catch {
168
+ return state;
169
+ }
170
+
171
+ const blocks = parseLegacyAgentDiffBlocks(raw);
172
+ const importedCount = state.legacy?.agentDiffImportedCount ?? 0;
173
+ if (blocks.length <= importedCount) {
174
+ // Already migrated before: remove legacy file to avoid stale duplicate source.
175
+ if (blocks.length > 0) removeLegacyFile(legacyPath);
176
+ return state;
177
+ }
178
+
179
+ const incoming = blocks.slice(importedCount);
180
+ const importedEvents: ContentChangeEvent[] = incoming.map((entry, idx) => {
181
+ const before = normalizeText(entry.before);
182
+ const after = normalizeText(entry.after);
183
+ const toolName = typeof entry.tool === 'string' && entry.tool.trim()
184
+ ? entry.tool.trim()
185
+ : 'unknown-tool';
186
+ const targetPath = typeof entry.path === 'string' && entry.path.trim()
187
+ ? entry.path
188
+ : 'Agent-Diff.md';
189
+ return {
190
+ id: `legacy-${Date.now().toString(36)}-${idx.toString(36)}`,
191
+ ts: toValidIso(entry.ts),
192
+ op: 'legacy_agent_diff_import',
193
+ path: targetPath,
194
+ source: 'agent',
195
+ summary: `Imported legacy agent diff (${toolName})`,
196
+ before: before.value,
197
+ after: after.value,
198
+ truncated: before.truncated || after.truncated || undefined,
199
+ };
200
+ });
201
+
202
+ const merged = [...state.events, ...importedEvents].sort(
203
+ (a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime(),
204
+ );
205
+
206
+ const nextState = {
207
+ ...state,
208
+ events: merged.slice(0, MAX_EVENTS),
209
+ legacy: {
210
+ agentDiffImportedCount: blocks.length,
211
+ lastImportedAt: nowIso(),
212
+ },
213
+ };
214
+ removeLegacyFile(legacyPath);
215
+ return nextState;
216
+ }
217
+
218
+ function loadState(mindRoot: string): ChangeLogState {
103
219
  const state = readState(mindRoot);
220
+ const migrated = importLegacyAgentDiffIfNeeded(mindRoot, state);
221
+ const changed =
222
+ (state.legacy?.agentDiffImportedCount ?? 0) !== (migrated.legacy?.agentDiffImportedCount ?? 0) ||
223
+ state.events.length !== migrated.events.length;
224
+ if (changed) {
225
+ writeState(mindRoot, migrated);
226
+ }
227
+ return migrated;
228
+ }
229
+
230
+ export function appendContentChange(mindRoot: string, input: ContentChangeInput): ContentChangeEvent {
231
+ const state = loadState(mindRoot);
104
232
  const before = normalizeText(input.before);
105
233
  const after = normalizeText(input.after);
106
234
  const event: ContentChangeEvent = {
@@ -125,23 +253,35 @@ export function appendContentChange(mindRoot: string, input: ContentChangeInput)
125
253
  }
126
254
 
127
255
  export function listContentChanges(mindRoot: string, options: ListOptions = {}): ContentChangeEvent[] {
128
- const state = readState(mindRoot);
256
+ const state = loadState(mindRoot);
129
257
  const limit = Math.max(1, Math.min(options.limit ?? 50, 200));
130
- const pathFilter = options.path;
131
- const events = pathFilter
132
- ? state.events.filter((event) => event.path === pathFilter || event.beforePath === pathFilter || event.afterPath === pathFilter)
133
- : state.events;
258
+ const pathFilter = options.path?.trim();
259
+ const sourceFilter = options.source;
260
+ const opFilter = options.op?.trim();
261
+ const q = options.q?.trim().toLowerCase();
262
+ const events = state.events.filter((event) => {
263
+ if (pathFilter && event.path !== pathFilter && event.beforePath !== pathFilter && event.afterPath !== pathFilter) {
264
+ return false;
265
+ }
266
+ if (sourceFilter && event.source !== sourceFilter) return false;
267
+ if (opFilter && event.op !== opFilter) return false;
268
+ if (q) {
269
+ const haystack = `${event.path} ${event.beforePath ?? ''} ${event.afterPath ?? ''} ${event.summary} ${event.op} ${event.source}`.toLowerCase();
270
+ if (!haystack.includes(q)) return false;
271
+ }
272
+ return true;
273
+ });
134
274
  return events.slice(0, limit);
135
275
  }
136
276
 
137
277
  export function markContentChangesSeen(mindRoot: string): void {
138
- const state = readState(mindRoot);
278
+ const state = loadState(mindRoot);
139
279
  state.lastSeenAt = nowIso();
140
280
  writeState(mindRoot, state);
141
281
  }
142
282
 
143
283
  export function getContentChangeSummary(mindRoot: string): ContentChangeSummary {
144
- const state = readState(mindRoot);
284
+ const state = loadState(mindRoot);
145
285
  const lastSeenAtMs = state.lastSeenAt ? new Date(state.lastSeenAt).getTime() : 0;
146
286
  const unreadCount = state.events.filter((event) => new Date(event.ts).getTime() > lastSeenAtMs).length;
147
287
  return {
package/app/lib/fs.ts CHANGED
@@ -199,7 +199,13 @@ export function appendContentChange(input: ContentChangeInput): ContentChangeEve
199
199
  return coreAppendContentChange(getMindRoot(), input);
200
200
  }
201
201
 
202
- export function listContentChanges(options: { path?: string; limit?: number } = {}): ContentChangeEvent[] {
202
+ export function listContentChanges(options: {
203
+ path?: string;
204
+ limit?: number;
205
+ source?: 'user' | 'agent' | 'system';
206
+ op?: string;
207
+ q?: string;
208
+ } = {}): ContentChangeEvent[] {
203
209
  return coreListContentChanges(getMindRoot(), options);
204
210
  }
205
211