@geminilight/mindos 0.5.22 → 0.5.24

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 (45) hide show
  1. package/app/app/api/ask/route.ts +7 -14
  2. package/app/app/api/bootstrap/route.ts +1 -0
  3. package/app/app/globals.css +14 -0
  4. package/app/app/setup/page.tsx +3 -2
  5. package/app/components/ActivityBar.tsx +183 -0
  6. package/app/components/AskFab.tsx +39 -97
  7. package/app/components/AskModal.tsx +13 -371
  8. package/app/components/Breadcrumb.tsx +4 -4
  9. package/app/components/FileTree.tsx +21 -4
  10. package/app/components/Logo.tsx +39 -0
  11. package/app/components/Panel.tsx +152 -0
  12. package/app/components/RightAskPanel.tsx +72 -0
  13. package/app/components/SettingsModal.tsx +9 -241
  14. package/app/components/SidebarLayout.tsx +426 -12
  15. package/app/components/SyncStatusBar.tsx +74 -53
  16. package/app/components/TableOfContents.tsx +4 -2
  17. package/app/components/ask/AskContent.tsx +418 -0
  18. package/app/components/ask/MessageList.tsx +2 -2
  19. package/app/components/panels/AgentsPanel.tsx +231 -0
  20. package/app/components/panels/PanelHeader.tsx +35 -0
  21. package/app/components/panels/PluginsPanel.tsx +106 -0
  22. package/app/components/panels/SearchPanel.tsx +178 -0
  23. package/app/components/panels/SyncPopover.tsx +105 -0
  24. package/app/components/renderers/csv/TableView.tsx +4 -4
  25. package/app/components/settings/AiTab.tsx +39 -1
  26. package/app/components/settings/KnowledgeTab.tsx +116 -2
  27. package/app/components/settings/McpTab.tsx +6 -6
  28. package/app/components/settings/SettingsContent.tsx +343 -0
  29. package/app/components/settings/types.ts +1 -1
  30. package/app/components/setup/index.tsx +2 -23
  31. package/app/hooks/useResizeDrag.ts +78 -0
  32. package/app/lib/agent/index.ts +0 -1
  33. package/app/lib/agent/model.ts +33 -10
  34. package/app/lib/format.ts +19 -0
  35. package/app/lib/i18n-en.ts +6 -6
  36. package/app/lib/i18n-zh.ts +5 -5
  37. package/app/next-env.d.ts +1 -1
  38. package/app/next.config.ts +1 -1
  39. package/bin/cli.js +27 -97
  40. package/package.json +4 -2
  41. package/scripts/setup.js +2 -12
  42. package/skills/mindos/SKILL.md +226 -8
  43. package/skills/mindos-zh/SKILL.md +226 -8
  44. package/app/lib/agent/skill-rules.ts +0 -70
  45. package/app/package-lock.json +0 -15736
@@ -13,7 +13,6 @@ import {
13
13
  createTransformContext,
14
14
  } from '@/lib/agent/context';
15
15
  import { logAgentOp } from '@/lib/agent/log';
16
- import { loadSkillRules } from '@/lib/agent/skill-rules';
17
16
  import { readSettings } from '@/lib/settings';
18
17
  import { MindOSError, apiError, ErrorCodes } from '@/lib/errors';
19
18
  import { metrics } from '@/lib/metrics';
@@ -179,17 +178,15 @@ export async function POST(req: NextRequest) {
179
178
  const contextStrategy = agentConfig.contextStrategy ?? 'auto';
180
179
 
181
180
  // Auto-load skill + bootstrap context for each request.
182
- // 1. SKILL.md — static trigger + protocol (always loaded)
183
- // 2. skill-rules.md — user's knowledge base operating rules (if exists)
184
- // 3. user-rules.md — user's personalized rules (if exists)
181
+ // 1. SKILL.md — complete skill with operating rules (always loaded)
182
+ // 2. user-skill-rules.md — user's personalized rules from KB root (if exists)
185
183
  const isZh = serverSettings.disabledSkills?.includes('mindos') ?? false;
186
184
  const skillDirName = isZh ? 'mindos-zh' : 'mindos';
187
185
  const skillPath = path.resolve(process.cwd(), `data/skills/${skillDirName}/SKILL.md`);
188
186
  const skill = readAbsoluteFile(skillPath);
189
187
 
190
- // Progressive skill loading: read skill-rules + user-rules from knowledge base
191
188
  const mindRoot = getMindRoot();
192
- const { skillRules, userRules } = loadSkillRules(mindRoot, skillDirName);
189
+ const userSkillRules = readKnowledgeFile('user-skill-rules.md');
193
190
 
194
191
  const targetDir = dirnameOf(currentFile);
195
192
  const bootstrap = {
@@ -208,8 +205,7 @@ export async function POST(req: NextRequest) {
208
205
  const truncationWarnings: string[] = [];
209
206
  if (!skill.ok) initFailures.push(`skill.mindos: failed (${skill.error})`);
210
207
  if (skill.ok && skill.truncated) truncationWarnings.push('skill.mindos was truncated');
211
- if (skillRules.ok && skillRules.truncated) truncationWarnings.push('skill-rules.md was truncated');
212
- if (userRules.ok && userRules.truncated) truncationWarnings.push('user-rules.md was truncated');
208
+ if (userSkillRules.ok && userSkillRules.truncated) truncationWarnings.push('user-skill-rules.md was truncated');
213
209
  if (!bootstrap.instruction.ok) initFailures.push(`bootstrap.instruction: failed (${bootstrap.instruction.error})`);
214
210
  if (bootstrap.instruction.ok && bootstrap.instruction.truncated) truncationWarnings.push('bootstrap.instruction was truncated');
215
211
  if (!bootstrap.index.ok) initFailures.push(`bootstrap.index: failed (${bootstrap.index.error})`);
@@ -233,12 +229,9 @@ export async function POST(req: NextRequest) {
233
229
 
234
230
  const initContextBlocks: string[] = [];
235
231
  if (skill.ok) initContextBlocks.push(`## mindos_skill_md\n\n${skill.content}`);
236
- // Progressive skill loading: inject skill-rules and user-rules after SKILL.md
237
- if (skillRules.ok && !skillRules.empty) {
238
- initContextBlocks.push(`## skill_rules\n\nOperating rules loaded from knowledge base (.agents/skills/${skillDirName}/skill-rules.md):\n\n${skillRules.content}`);
239
- }
240
- if (userRules.ok && !userRules.empty) {
241
- initContextBlocks.push(`## user_rules\n\nUser personalization rules (.agents/skills/${skillDirName}/user-rules.md):\n\n${userRules.content}`);
232
+ // User personalization rules (from knowledge base root)
233
+ if (userSkillRules.ok && !userSkillRules.truncated && userSkillRules.content.trim()) {
234
+ initContextBlocks.push(`## user_skill_rules\n\nUser personalization rules (user-skill-rules.md):\n\n${userSkillRules.content}`);
242
235
  }
243
236
  if (bootstrap.instruction.ok) initContextBlocks.push(`## bootstrap_instruction\n\n${bootstrap.instruction.content}`);
244
237
  if (bootstrap.index.ok) initContextBlocks.push(`## bootstrap_index\n\n${bootstrap.index.content}`);
@@ -21,6 +21,7 @@ export async function GET(req: NextRequest) {
21
21
  index: tryRead('README.md'),
22
22
  config_json: tryRead('CONFIG.json'),
23
23
  config_md: tryRead('CONFIG.md'),
24
+ user_skill_rules: tryRead('user-skill-rules.md'),
24
25
  };
25
26
 
26
27
  if (targetDir) {
@@ -212,6 +212,20 @@ body {
212
212
  .prose img { max-width: 100%; border-radius: 6px; border: 1px solid var(--prose-code-border); }
213
213
  .prose hr { border: none; border-top: 1px solid var(--prose-border); margin: 2.5em 0; }
214
214
 
215
+ /* Ask Panel — compact prose for side panel chat bubbles */
216
+ .prose.prose-panel {
217
+ font-size: 0.8125rem !important;
218
+ line-height: 1.6;
219
+ }
220
+ .prose.prose-panel h1, .prose.prose-panel h2, .prose.prose-panel h3, .prose.prose-panel h4 {
221
+ font-size: 0.8125rem !important;
222
+ margin-top: 1em;
223
+ margin-bottom: 0.4em;
224
+ }
225
+ .prose.prose-panel p { margin-bottom: 0.6em; }
226
+ .prose.prose-panel pre { padding: 0.6em 0.8em; margin: 0.8em 0; }
227
+ .prose.prose-panel pre code { font-size: 0.78em; }
228
+
215
229
  :root {
216
230
  --prose-body: #3a3730;
217
231
  --prose-heading: #1c1a17;
@@ -4,9 +4,10 @@ import SetupWizard from '@/components/SetupWizard';
4
4
 
5
5
  export const dynamic = 'force-dynamic';
6
6
 
7
- export default function SetupPage({ searchParams }: { searchParams: { force?: string } }) {
7
+ export default async function SetupPage({ searchParams }: { searchParams: Promise<{ force?: string }> }) {
8
8
  const settings = readSettings();
9
- const force = searchParams.force === '1';
9
+ const { force: forceParam } = await searchParams;
10
+ const force = forceParam === '1';
10
11
  if (!settings.setupPending && !force) redirect('/');
11
12
  return <SetupWizard />;
12
13
  }
@@ -0,0 +1,183 @@
1
+ 'use client';
2
+
3
+ import { useRef, useCallback } from 'react';
4
+ import Link from 'next/link';
5
+ import { FolderTree, Search, Settings, RefreshCw, Blocks, Bot, ChevronLeft, ChevronRight } from 'lucide-react';
6
+ import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
7
+ import type { SyncStatus } from './settings/SyncTab';
8
+ import Logo from './Logo';
9
+
10
+ export type PanelId = 'files' | 'search' | 'plugins' | 'agents';
11
+
12
+ export const RAIL_WIDTH_COLLAPSED = 48;
13
+ export const RAIL_WIDTH_EXPANDED = 180;
14
+
15
+ interface ActivityBarProps {
16
+ activePanel: PanelId | null;
17
+ onPanelChange: (id: PanelId | null) => void;
18
+ syncStatus: SyncStatus | null;
19
+ expanded: boolean;
20
+ onExpandedChange: (expanded: boolean) => void;
21
+ onSettingsClick: () => void;
22
+ onSyncClick: (rect: DOMRect) => void;
23
+ }
24
+
25
+ interface RailButtonProps {
26
+ icon: React.ReactNode;
27
+ label: string;
28
+ shortcut?: string;
29
+ active?: boolean;
30
+ expanded: boolean;
31
+ onClick: () => void;
32
+ buttonRef?: React.Ref<HTMLButtonElement>;
33
+ /** Optional overlay badge (e.g. status dot) rendered inside the button */
34
+ badge?: React.ReactNode;
35
+ }
36
+
37
+ function RailButton({ icon, label, shortcut, active = false, expanded, onClick, buttonRef, badge }: RailButtonProps) {
38
+ return (
39
+ <button
40
+ ref={buttonRef}
41
+ onClick={onClick}
42
+ aria-pressed={active}
43
+ aria-label={label}
44
+ title={expanded ? undefined : (shortcut ? `${label} (${shortcut})` : label)}
45
+ className={`
46
+ relative flex items-center ${expanded ? 'justify-start px-3 w-full' : 'justify-center w-10'} h-10 rounded-md transition-colors
47
+ ${active
48
+ ? 'text-[var(--amber)] bg-[var(--amber-dim)]'
49
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
50
+ }
51
+ focus-visible:ring-2 focus-visible:ring-ring
52
+ `}
53
+ >
54
+ {active && (
55
+ <span className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-[18px] rounded-r-full" style={{ background: 'var(--amber)' }} />
56
+ )}
57
+ <span className="shrink-0 flex items-center justify-center w-[18px]">{icon}</span>
58
+ {badge}
59
+ {expanded && (
60
+ <>
61
+ <span className="ml-2.5 text-sm whitespace-nowrap">{label}</span>
62
+ {shortcut && (
63
+ <span className="ml-auto text-2xs text-muted-foreground/60 font-mono shrink-0">{shortcut}</span>
64
+ )}
65
+ </>
66
+ )}
67
+ </button>
68
+ );
69
+ }
70
+
71
+ export default function ActivityBar({
72
+ activePanel,
73
+ onPanelChange,
74
+ syncStatus,
75
+ expanded,
76
+ onExpandedChange,
77
+ onSettingsClick,
78
+ onSyncClick,
79
+ }: ActivityBarProps) {
80
+ const lastClickRef = useRef(0);
81
+ const syncBtnRef = useRef<HTMLButtonElement>(null);
82
+
83
+ /** Debounce rapid clicks (300ms) — shared across all Rail buttons */
84
+ const debounced = useCallback((fn: () => void) => {
85
+ const now = Date.now();
86
+ if (now - lastClickRef.current < 300) return;
87
+ lastClickRef.current = now;
88
+ fn();
89
+ }, []);
90
+
91
+ const toggle = useCallback((id: PanelId) => {
92
+ debounced(() => onPanelChange(activePanel === id ? null : id));
93
+ }, [activePanel, onPanelChange, debounced]);
94
+
95
+ const syncLevel = getStatusLevel(syncStatus, false);
96
+ const showSyncDot = syncLevel !== 'off' && syncLevel !== 'synced';
97
+
98
+ const railWidth = expanded ? RAIL_WIDTH_EXPANDED : RAIL_WIDTH_COLLAPSED;
99
+
100
+ // Sync dot badge — positioned differently in collapsed vs expanded
101
+ const syncBadge = showSyncDot ? (
102
+ <span className={`absolute ${expanded ? 'left-[26px] top-1.5' : 'top-1.5 right-1.5'} w-2 h-2 rounded-full ${DOT_COLORS[syncLevel]} ${syncLevel === 'error' || syncLevel === 'conflicts' ? 'animate-pulse' : ''}`} />
103
+ ) : undefined;
104
+
105
+ return (
106
+ <aside
107
+ className="group hidden md:flex fixed top-0 left-0 h-screen z-[31] flex-col bg-background border-r border-border transition-[width] duration-200 ease-out"
108
+ style={{ width: `${railWidth}px` }}
109
+ role="toolbar"
110
+ aria-label="Navigation"
111
+ aria-orientation="vertical"
112
+ >
113
+ {/* Content wrapper — overflow-hidden prevents text flash during width transitions */}
114
+ <div className="flex flex-col h-full w-full overflow-hidden">
115
+ {/* ── Top: Logo ── */}
116
+ <Link
117
+ href="/"
118
+ className={`flex items-center ${expanded ? 'px-3 gap-2' : 'justify-center'} w-full py-3 hover:opacity-80 transition-opacity`}
119
+ aria-label="MindOS Home"
120
+ >
121
+ <Logo id="rail" className="w-7 h-3.5 shrink-0" />
122
+ {expanded && <span className="text-sm font-semibold text-foreground font-display whitespace-nowrap">MindOS</span>}
123
+ </Link>
124
+
125
+ <div className={`${expanded ? 'mx-3' : 'mx-auto w-6'} border-t border-border`} />
126
+
127
+ {/* ── Middle: Core panel toggles ── */}
128
+ <div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
129
+ <RailButton icon={<FolderTree size={18} />} label="Files" active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} />
130
+ <RailButton icon={<Search size={18} />} label="Search" shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
131
+ <RailButton icon={<Blocks size={18} />} label="Plugins" active={activePanel === 'plugins'} expanded={expanded} onClick={() => toggle('plugins')} />
132
+ <RailButton icon={<Bot size={18} />} label="Agents" active={activePanel === 'agents'} expanded={expanded} onClick={() => toggle('agents')} />
133
+ </div>
134
+
135
+ {/* ── Spacer ── */}
136
+ <div className="flex-1" />
137
+
138
+ {/* ── Bottom: Action buttons (not panel toggles) ── */}
139
+ <div className={`${expanded ? 'mx-3' : 'mx-auto w-6'} border-t border-border`} />
140
+ <div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
141
+ <RailButton
142
+ icon={<Settings size={18} />}
143
+ label="Settings"
144
+ shortcut="⌘,"
145
+ expanded={expanded}
146
+ onClick={() => debounced(onSettingsClick)}
147
+ />
148
+ <RailButton
149
+ icon={<RefreshCw size={18} />}
150
+ label="Sync"
151
+ expanded={expanded}
152
+ buttonRef={syncBtnRef}
153
+ badge={syncBadge}
154
+ onClick={() => debounced(() => {
155
+ const rect = syncBtnRef.current?.getBoundingClientRect();
156
+ if (rect) onSyncClick(rect);
157
+ })}
158
+ />
159
+ </div>
160
+ </div>
161
+
162
+ {/* ── Hover expand/collapse button — vertically centered on right edge ── */}
163
+ {/* z-[32] ensures it paints above Panel (z-30). Shows on Rail hover OR self-hover. */}
164
+ <button
165
+ onClick={() => onExpandedChange(!expanded)}
166
+ className="
167
+ absolute right-0 top-1/2 -translate-y-1/2 translate-x-1/2 z-[32]
168
+ w-5 h-5 rounded-full
169
+ bg-card border border-border shadow-sm
170
+ flex items-center justify-center
171
+ opacity-0 group-hover:opacity-100 hover:!opacity-100
172
+ transition-opacity duration-200
173
+ text-muted-foreground hover:text-foreground hover:bg-muted
174
+ focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring
175
+ "
176
+ aria-label={expanded ? 'Collapse sidebar' : 'Expand sidebar'}
177
+ title={expanded ? 'Collapse' : 'Expand'}
178
+ >
179
+ {expanded ? <ChevronLeft size={10} /> : <ChevronRight size={10} />}
180
+ </button>
181
+ </aside>
182
+ );
183
+ }
@@ -1,105 +1,47 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback } from 'react';
4
- import { usePathname } from 'next/navigation';
5
3
  import { Sparkles } from 'lucide-react';
6
- import AskModal from './AskModal';
7
- import { useAskModal } from '@/hooks/useAskModal';
8
4
 
9
- export default function AskFab() {
10
- const [open, setOpen] = useState(false);
11
- const pathname = usePathname();
12
- const currentFile = pathname.startsWith('/view/')
13
- ? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
14
- : undefined;
15
-
16
- // Listen to useAskModal store for cross-component open requests (e.g. from GuideCard)
17
- const askModal = useAskModal();
18
- const [initialMessage, setInitialMessage] = useState('');
19
- const [openSource, setOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
20
-
21
- useEffect(() => {
22
- if (askModal.open) {
23
- setInitialMessage(askModal.initialMessage);
24
- setOpenSource(askModal.source);
25
- setOpen(true);
26
- askModal.close(); // Reset store state after consuming
27
- }
28
- }, [askModal.open, askModal.initialMessage, askModal.source, askModal.close]);
29
-
30
- const handleClose = useCallback(() => {
31
- setOpen(false);
32
- setInitialMessage('');
33
- setOpenSource('user');
34
- }, []);
35
-
36
- // Dispatch correct PATCH based on how the modal was opened
37
- const handleFirstMessage = useCallback(() => {
38
- const notifyGuide = () => window.dispatchEvent(new Event('guide-state-updated'));
39
-
40
- if (openSource === 'guide') {
41
- // Task ② completion: mark askedAI
42
- fetch('/api/setup', {
43
- method: 'PATCH',
44
- headers: { 'Content-Type': 'application/json' },
45
- body: JSON.stringify({ guideState: { askedAI: true } }),
46
- }).then(notifyGuide).catch(() => {});
47
- } else if (openSource === 'guide-next') {
48
- // Next-step advancement: GuideCard already PATCHed nextStepIndex optimistically.
49
- // Just notify GuideCard to re-fetch for consistency; no additional PATCH needed.
50
- notifyGuide();
51
- }
52
- // For 'user' source: no guide action needed
53
- }, [openSource]);
5
+ interface AskFabProps {
6
+ /** Toggle the right-side Ask AI panel */
7
+ onToggle: () => void;
8
+ /** Whether the right panel is currently open (FAB hides when open) */
9
+ askPanelOpen: boolean;
10
+ }
54
11
 
12
+ export default function AskFab({ onToggle, askPanelOpen }: AskFabProps) {
55
13
  return (
56
- <>
57
- <button
58
- onClick={() => { setInitialMessage(''); setOpenSource('user'); setOpen(true); }}
59
- className="
60
- group
61
- fixed z-40
62
- bottom-5 right-5
63
- md:bottom-5 md:right-5
64
- flex items-center justify-center
65
- gap-0 hover:gap-2
66
- p-3 md:p-[11px] rounded-xl
67
- text-white font-medium text-[13px]
68
- shadow-md shadow-amber-900/15
69
- transition-all duration-200 ease-out
70
- hover:shadow-lg hover:shadow-amber-800/25
71
- active:scale-95
72
- cursor-pointer
73
- overflow-hidden
74
- font-display
75
- "
76
- style={{
77
- background: 'linear-gradient(135deg, #b07c2e 0%, #c8873a 50%, #d4943f 100%)',
78
- marginBottom: 'env(safe-area-inset-bottom, 0px)',
79
- }}
80
- title="MindOS Agent (⌘/)"
81
- aria-label="MindOS Agent"
82
- >
83
- <Sparkles size={16} className="relative z-10 shrink-0" />
84
-
85
- <span className="
86
- relative z-10
87
- max-w-0 group-hover:max-w-[120px]
88
- opacity-0 group-hover:opacity-100
89
- transition-all duration-200 ease-out
90
- whitespace-nowrap overflow-hidden
91
- ">
92
- MindOS Agent
93
- </span>
94
- </button>
95
-
96
- <AskModal
97
- open={open}
98
- onClose={handleClose}
99
- currentFile={currentFile}
100
- initialMessage={initialMessage}
101
- onFirstMessage={handleFirstMessage}
102
- />
103
- </>
14
+ <button
15
+ onClick={onToggle}
16
+ className={`
17
+ group hidden md:flex
18
+ fixed z-40 bottom-5 right-5
19
+ items-center justify-center
20
+ gap-0 hover:gap-2
21
+ p-[11px] rounded-xl
22
+ text-white font-medium text-[13px]
23
+ shadow-md shadow-amber-900/15
24
+ transition-all duration-200 ease-out
25
+ hover:shadow-lg hover:shadow-amber-800/25
26
+ active:scale-95 cursor-pointer overflow-hidden font-display
27
+ ${askPanelOpen ? 'opacity-0 pointer-events-none translate-y-2' : 'opacity-100 translate-y-0'}
28
+ `}
29
+ style={{
30
+ background: 'linear-gradient(135deg, #b07c2e 0%, #c8873a 50%, #d4943f 100%)',
31
+ }}
32
+ title="MindOS Agent (⌘/)"
33
+ aria-label="MindOS Agent"
34
+ >
35
+ <Sparkles size={16} className="relative z-10 shrink-0" />
36
+ <span className="
37
+ relative z-10
38
+ max-w-0 group-hover:max-w-[120px]
39
+ opacity-0 group-hover:opacity-100
40
+ transition-all duration-200 ease-out
41
+ whitespace-nowrap overflow-hidden
42
+ ">
43
+ MindOS Agent
44
+ </span>
45
+ </button>
104
46
  );
105
47
  }