@geminilight/mindos 0.5.10 → 0.5.12
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.
- package/README.md +9 -9
- package/README_zh.md +9 -9
- package/app/README.md +2 -2
- package/app/app/api/ask/route.ts +126 -3
- package/app/app/api/mcp/install/route.ts +1 -1
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/settings/route.ts +1 -1
- package/app/app/api/settings/test-key/route.ts +111 -0
- package/app/app/api/setup/route.ts +7 -7
- package/app/app/api/sync/route.ts +30 -40
- package/app/components/AskModal.tsx +28 -21
- package/app/components/ask/MessageList.tsx +62 -3
- package/app/components/ask/ToolCallBlock.tsx +89 -0
- package/app/components/settings/AiTab.tsx +120 -2
- package/app/components/setup/StepReview.tsx +31 -25
- package/app/components/setup/index.tsx +6 -3
- package/app/instrumentation.ts +19 -0
- package/app/lib/agent/prompt.ts +32 -0
- package/app/lib/agent/stream-consumer.ts +178 -0
- package/app/lib/agent/tools.ts +122 -0
- package/app/lib/i18n.ts +18 -0
- package/app/lib/types.ts +18 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -2
- package/bin/cli.js +49 -22
- package/bin/lib/gateway.js +24 -3
- package/bin/lib/mcp-install.js +2 -2
- package/bin/lib/mcp-spawn.js +3 -3
- package/bin/lib/stop.js +1 -1
- package/bin/lib/sync.js +61 -11
- package/mcp/README.md +5 -5
- package/mcp/src/index.ts +2 -2
- package/package.json +4 -2
- package/scripts/setup.js +12 -12
- package/scripts/upgrade-prompt.md +6 -6
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/assets/images/gui-sync-cv.png +0 -0
- package/assets/images/wechat-qr.png +0 -0
- package/mcp/package-lock.json +0 -1717
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useRef, useEffect } from 'react';
|
|
4
|
-
import { Sparkles, Loader2, AlertCircle } from 'lucide-react';
|
|
4
|
+
import { Sparkles, Loader2, AlertCircle, Wrench } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
7
|
import type { Message } from '@/lib/types';
|
|
8
|
+
import ToolCallBlock from './ToolCallBlock';
|
|
8
9
|
|
|
9
10
|
function AssistantMessage({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
10
11
|
return (
|
|
@@ -28,6 +29,57 @@ function AssistantMessage({ content, isStreaming }: { content: string; isStreami
|
|
|
28
29
|
);
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function AssistantMessageWithParts({ message, isStreaming }: { message: Message; isStreaming: boolean }) {
|
|
33
|
+
const parts = message.parts;
|
|
34
|
+
if (!parts || parts.length === 0) {
|
|
35
|
+
// Fallback to plain text rendering
|
|
36
|
+
return message.content ? (
|
|
37
|
+
<AssistantMessage content={message.content} isStreaming={isStreaming} />
|
|
38
|
+
) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if the last part is a running tool call — show a spinner after it
|
|
42
|
+
const lastPart = parts[parts.length - 1];
|
|
43
|
+
const showTrailingSpinner = isStreaming && lastPart.type === 'tool-call' && (lastPart.state === 'running' || lastPart.state === 'pending');
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
{parts.map((part, idx) => {
|
|
48
|
+
if (part.type === 'text') {
|
|
49
|
+
const isLastTextPart = isStreaming && idx === parts.length - 1;
|
|
50
|
+
return part.text ? (
|
|
51
|
+
<AssistantMessage key={idx} content={part.text} isStreaming={isLastTextPart} />
|
|
52
|
+
) : null;
|
|
53
|
+
}
|
|
54
|
+
if (part.type === 'tool-call') {
|
|
55
|
+
return <ToolCallBlock key={part.toolCallId} part={part} />;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
})}
|
|
59
|
+
{showTrailingSpinner && (
|
|
60
|
+
<div className="flex items-center gap-2 py-1 mt-1">
|
|
61
|
+
<Loader2 size={12} className="animate-spin" style={{ color: 'var(--amber)' }} />
|
|
62
|
+
<span className="text-xs text-muted-foreground animate-pulse">Executing tool…</span>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function StepCounter({ parts, maxSteps }: { parts: Message['parts']; maxSteps?: number }) {
|
|
70
|
+
if (!parts) return null;
|
|
71
|
+
const toolCalls = parts.filter(p => p.type === 'tool-call');
|
|
72
|
+
if (toolCalls.length === 0) return null;
|
|
73
|
+
const lastToolCall = toolCalls[toolCalls.length - 1];
|
|
74
|
+
const toolLabel = lastToolCall.type === 'tool-call' ? lastToolCall.toolName : '';
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex items-center gap-1.5 mt-1.5 text-xs text-muted-foreground/70">
|
|
77
|
+
<Wrench size={10} />
|
|
78
|
+
<span>Step {toolCalls.length}{maxSteps ? `/${maxSteps}` : ''}{toolLabel ? ` — ${toolLabel}` : ''}</span>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
interface MessageListProps {
|
|
32
84
|
messages: Message[];
|
|
33
85
|
isLoading: boolean;
|
|
@@ -35,6 +87,7 @@ interface MessageListProps {
|
|
|
35
87
|
emptyPrompt: string;
|
|
36
88
|
suggestions: readonly string[];
|
|
37
89
|
onSuggestionClick: (text: string) => void;
|
|
90
|
+
maxSteps?: number;
|
|
38
91
|
labels: {
|
|
39
92
|
connecting: string;
|
|
40
93
|
thinking: string;
|
|
@@ -49,6 +102,7 @@ export default function MessageList({
|
|
|
49
102
|
emptyPrompt,
|
|
50
103
|
suggestions,
|
|
51
104
|
onSuggestionClick,
|
|
105
|
+
maxSteps,
|
|
52
106
|
labels,
|
|
53
107
|
}: MessageListProps) {
|
|
54
108
|
const endRef = useRef<HTMLDivElement>(null);
|
|
@@ -102,8 +156,13 @@ export default function MessageList({
|
|
|
102
156
|
</div>
|
|
103
157
|
) : (
|
|
104
158
|
<div className="max-w-[85%] px-3 py-2 rounded-xl rounded-bl-sm bg-muted text-foreground text-sm">
|
|
105
|
-
{m.content ? (
|
|
106
|
-
|
|
159
|
+
{(m.parts && m.parts.length > 0) || m.content ? (
|
|
160
|
+
<>
|
|
161
|
+
<AssistantMessageWithParts message={m} isStreaming={isLoading && i === messages.length - 1} />
|
|
162
|
+
{isLoading && i === messages.length - 1 && (
|
|
163
|
+
<StepCounter parts={m.parts} maxSteps={maxSteps} />
|
|
164
|
+
)}
|
|
165
|
+
</>
|
|
107
166
|
) : isLoading && i === messages.length - 1 ? (
|
|
108
167
|
<div className="flex items-center gap-2 py-1">
|
|
109
168
|
<Loader2 size={14} className="animate-spin" style={{ color: 'var(--amber)' }} />
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ChevronRight, ChevronDown, Loader2, CheckCircle2, XCircle } from 'lucide-react';
|
|
5
|
+
import type { ToolCallPart } from '@/lib/types';
|
|
6
|
+
|
|
7
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
8
|
+
search: '🔍',
|
|
9
|
+
list_files: '📂',
|
|
10
|
+
read_file: '📖',
|
|
11
|
+
write_file: '✏️',
|
|
12
|
+
create_file: '📄',
|
|
13
|
+
append_to_file: '📝',
|
|
14
|
+
insert_after_heading: '📌',
|
|
15
|
+
update_section: '✏️',
|
|
16
|
+
delete_file: '🗑️',
|
|
17
|
+
rename_file: '📝',
|
|
18
|
+
move_file: '📦',
|
|
19
|
+
get_backlinks: '🔗',
|
|
20
|
+
get_history: '📜',
|
|
21
|
+
get_file_at_version: '⏪',
|
|
22
|
+
get_recent: '🕐',
|
|
23
|
+
append_csv: '📊',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function formatInput(input: unknown): string {
|
|
27
|
+
if (!input || typeof input !== 'object') return String(input ?? '');
|
|
28
|
+
const obj = input as Record<string, unknown>;
|
|
29
|
+
const parts: string[] = [];
|
|
30
|
+
for (const val of Object.values(obj)) {
|
|
31
|
+
if (typeof val === 'string') {
|
|
32
|
+
parts.push(val.length > 60 ? `${val.slice(0, 60)}…` : val);
|
|
33
|
+
} else if (Array.isArray(val)) {
|
|
34
|
+
parts.push(`[${val.length} items]`);
|
|
35
|
+
} else if (val !== undefined && val !== null) {
|
|
36
|
+
parts.push(String(val));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return parts.join(', ');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function truncateOutput(output: string, maxLen = 200): string {
|
|
43
|
+
if (output.length <= maxLen) return output;
|
|
44
|
+
return output.slice(0, maxLen) + '…';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
|
|
48
|
+
const [expanded, setExpanded] = useState(false);
|
|
49
|
+
const icon = TOOL_ICONS[part.toolName] ?? '🔧';
|
|
50
|
+
const inputSummary = formatInput(part.input);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="my-1 rounded-md border border-border/50 bg-muted/30 text-xs font-mono">
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => setExpanded(v => !v)}
|
|
57
|
+
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/50 transition-colors rounded-md"
|
|
58
|
+
>
|
|
59
|
+
{expanded ? <ChevronDown size={12} className="shrink-0 text-muted-foreground" /> : <ChevronRight size={12} className="shrink-0 text-muted-foreground" />}
|
|
60
|
+
<span>{icon}</span>
|
|
61
|
+
<span className="text-foreground font-medium">{part.toolName}</span>
|
|
62
|
+
<span className="text-muted-foreground truncate flex-1">({inputSummary})</span>
|
|
63
|
+
<span className="shrink-0 ml-auto">
|
|
64
|
+
{part.state === 'pending' || part.state === 'running' ? (
|
|
65
|
+
<Loader2 size={12} className="animate-spin text-amber-500" />
|
|
66
|
+
) : part.state === 'done' ? (
|
|
67
|
+
<CheckCircle2 size={12} className="text-success" />
|
|
68
|
+
) : (
|
|
69
|
+
<XCircle size={12} className="text-error" />
|
|
70
|
+
)}
|
|
71
|
+
</span>
|
|
72
|
+
</button>
|
|
73
|
+
{expanded && (
|
|
74
|
+
<div className="px-2 pb-2 pt-0.5 border-t border-border/30 space-y-1">
|
|
75
|
+
<div className="text-muted-foreground">
|
|
76
|
+
<span className="font-semibold">Input: </span>
|
|
77
|
+
<span className="break-all whitespace-pre-wrap">{JSON.stringify(part.input, null, 2)}</span>
|
|
78
|
+
</div>
|
|
79
|
+
{part.output !== undefined && (
|
|
80
|
+
<div className="text-muted-foreground">
|
|
81
|
+
<span className="font-semibold">Output: </span>
|
|
82
|
+
<span className="break-all whitespace-pre-wrap">{truncateOutput(part.output)}</span>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
4
5
|
import type { AiSettings, ProviderConfig, SettingsData } from './types';
|
|
5
6
|
import { Field, Select, Input, EnvBadge, ApiKeyInput } from './Primitives';
|
|
6
7
|
|
|
8
|
+
type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
9
|
+
type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
10
|
+
|
|
11
|
+
interface TestResult {
|
|
12
|
+
state: TestState;
|
|
13
|
+
latency?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
code?: ErrorCode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function errorMessage(t: any, code?: ErrorCode): string {
|
|
19
|
+
switch (code) {
|
|
20
|
+
case 'auth_error': return t.settings.ai.testKeyAuthError;
|
|
21
|
+
case 'model_not_found': return t.settings.ai.testKeyModelNotFound;
|
|
22
|
+
case 'rate_limited': return t.settings.ai.testKeyRateLimited;
|
|
23
|
+
case 'network_error': return t.settings.ai.testKeyNetworkError;
|
|
24
|
+
default: return t.settings.ai.testKeyUnknown;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
interface AiTabProps {
|
|
8
29
|
data: SettingsData;
|
|
9
30
|
updateAi: (patch: Partial<AiSettings>) => void;
|
|
@@ -15,13 +36,76 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
15
36
|
const envVal = data.envValues ?? {};
|
|
16
37
|
const provider = data.ai.provider;
|
|
17
38
|
|
|
18
|
-
|
|
39
|
+
// --- Test key state ---
|
|
40
|
+
const [testResult, setTestResult] = useState<Record<string, TestResult>>({});
|
|
41
|
+
const okTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
42
|
+
const prevProviderRef = useRef(provider);
|
|
43
|
+
|
|
44
|
+
// Reset test result when provider changes
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (prevProviderRef.current !== provider) {
|
|
47
|
+
prevProviderRef.current = provider;
|
|
48
|
+
setTestResult({});
|
|
49
|
+
if (okTimerRef.current) { clearTimeout(okTimerRef.current); okTimerRef.current = undefined; }
|
|
50
|
+
}
|
|
51
|
+
}, [provider]);
|
|
52
|
+
|
|
53
|
+
// Cleanup ok timer
|
|
54
|
+
useEffect(() => () => { if (okTimerRef.current) clearTimeout(okTimerRef.current); }, []);
|
|
55
|
+
|
|
56
|
+
const handleTestKey = useCallback(async (providerName: 'anthropic' | 'openai') => {
|
|
57
|
+
const prov = data.ai.providers?.[providerName] ?? {} as ProviderConfig;
|
|
58
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'testing' } }));
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const body: Record<string, string> = { provider: providerName };
|
|
62
|
+
if (prov.apiKey) body.apiKey = prov.apiKey;
|
|
63
|
+
if (prov.model) body.model = prov.model;
|
|
64
|
+
if (providerName === 'openai' && prov.baseUrl) body.baseUrl = prov.baseUrl;
|
|
65
|
+
|
|
66
|
+
const res = await fetch('/api/settings/test-key', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
});
|
|
71
|
+
const json = await res.json();
|
|
72
|
+
|
|
73
|
+
if (json.ok) {
|
|
74
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
|
|
75
|
+
// Auto-clear after 5s
|
|
76
|
+
if (okTimerRef.current) clearTimeout(okTimerRef.current);
|
|
77
|
+
okTimerRef.current = setTimeout(() => {
|
|
78
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
|
|
79
|
+
}, 5000);
|
|
80
|
+
} else {
|
|
81
|
+
setTestResult(prev => ({
|
|
82
|
+
...prev,
|
|
83
|
+
[providerName]: { state: 'error', error: json.error, code: json.code },
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
setTestResult(prev => ({
|
|
88
|
+
...prev,
|
|
89
|
+
[providerName]: { state: 'error', code: 'network_error', error: 'Network error' },
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
}, [data.ai.providers]);
|
|
93
|
+
|
|
94
|
+
// Reset test result when key changes
|
|
95
|
+
const patchProviderWithReset = useCallback((name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) => {
|
|
96
|
+
if ('apiKey' in patch) {
|
|
97
|
+
setTestResult(prev => ({ ...prev, [name]: { state: 'idle' } }));
|
|
98
|
+
}
|
|
19
99
|
updateAi({
|
|
20
100
|
providers: {
|
|
21
101
|
...data.ai.providers,
|
|
22
102
|
[name]: { ...data.ai.providers?.[name], ...patch },
|
|
23
103
|
},
|
|
24
104
|
});
|
|
105
|
+
}, [data.ai.providers, updateAi]);
|
|
106
|
+
|
|
107
|
+
function patchProvider(name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) {
|
|
108
|
+
patchProviderWithReset(name, patch);
|
|
25
109
|
}
|
|
26
110
|
|
|
27
111
|
const anthropic = data.ai.providers?.anthropic ?? { apiKey: '', model: '' };
|
|
@@ -31,6 +115,38 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
31
115
|
const activeEnvKey = provider === 'anthropic' ? env.ANTHROPIC_API_KEY : env.OPENAI_API_KEY;
|
|
32
116
|
const missingApiKey = !activeApiKey && !activeEnvKey;
|
|
33
117
|
|
|
118
|
+
// Test button helper
|
|
119
|
+
const renderTestButton = (providerName: 'anthropic' | 'openai', hasKey: boolean, hasEnv: boolean) => {
|
|
120
|
+
const result = testResult[providerName] ?? { state: 'idle' as TestState };
|
|
121
|
+
const disabled = result.state === 'testing' || (!hasKey && !hasEnv);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex items-center gap-2 mt-1.5">
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
onClick={() => handleTestKey(providerName)}
|
|
129
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
130
|
+
>
|
|
131
|
+
{result.state === 'testing' ? (
|
|
132
|
+
<>
|
|
133
|
+
<Loader2 size={12} className="animate-spin" />
|
|
134
|
+
{t.settings.ai.testKeyTesting}
|
|
135
|
+
</>
|
|
136
|
+
) : (
|
|
137
|
+
t.settings.ai.testKey
|
|
138
|
+
)}
|
|
139
|
+
</button>
|
|
140
|
+
{result.state === 'ok' && result.latency != null && (
|
|
141
|
+
<span className="text-xs text-success">{t.settings.ai.testKeyOk(result.latency)}</span>
|
|
142
|
+
)}
|
|
143
|
+
{result.state === 'error' && (
|
|
144
|
+
<span className="text-xs text-error">✗ {errorMessage(t, result.code)}</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
34
150
|
return (
|
|
35
151
|
<div className="space-y-5">
|
|
36
152
|
<Field label={<>{t.settings.ai.provider} <EnvBadge overridden={env.AI_PROVIDER} /></>}>
|
|
@@ -60,6 +176,7 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
60
176
|
value={anthropic.apiKey}
|
|
61
177
|
onChange={v => patchProvider('anthropic', { apiKey: v })}
|
|
62
178
|
/>
|
|
179
|
+
{renderTestButton('anthropic', !!anthropic.apiKey, !!env.ANTHROPIC_API_KEY)}
|
|
63
180
|
</Field>
|
|
64
181
|
</>
|
|
65
182
|
) : (
|
|
@@ -79,6 +196,7 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
79
196
|
value={openai.apiKey}
|
|
80
197
|
onChange={v => patchProvider('openai', { apiKey: v })}
|
|
81
198
|
/>
|
|
199
|
+
{renderTestButton('openai', !!openai.apiKey, !!env.OPENAI_API_KEY)}
|
|
82
200
|
</Field>
|
|
83
201
|
<Field
|
|
84
202
|
label={<>{t.settings.ai.baseUrl} <EnvBadge overridden={env.OPENAI_BASE_URL} /></>}
|
|
@@ -7,7 +7,24 @@ import {
|
|
|
7
7
|
import type { SetupState, SetupMessages, AgentInstallStatus } from './types';
|
|
8
8
|
|
|
9
9
|
// ─── Restart Block ────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
/** Restart warning banner — shown in the content area */
|
|
12
|
+
export function RestartBanner({ s }: { s: SetupMessages }) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-2">
|
|
15
|
+
<div className="p-3 rounded-lg text-sm flex items-center gap-2"
|
|
16
|
+
style={{ background: 'color-mix(in srgb, var(--amber) 10%, transparent)', color: 'var(--amber)' }}>
|
|
17
|
+
<AlertTriangle size={14} /> {s.restartRequired}
|
|
18
|
+
</div>
|
|
19
|
+
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
|
|
20
|
+
{s.restartManual} <code className="font-mono">mindos start</code>
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Restart button — shown in the bottom navigation bar (same position as Complete/Saving button) */
|
|
27
|
+
export function RestartButton({ s, newPort }: { s: SetupMessages; newPort: number }) {
|
|
11
28
|
const [restarting, setRestarting] = useState(false);
|
|
12
29
|
const [done, setDone] = useState(false);
|
|
13
30
|
const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
|
@@ -40,34 +57,23 @@ function RestartBlock({ s, newPort }: { s: SetupMessages; newPort: number }) {
|
|
|
40
57
|
|
|
41
58
|
if (done) {
|
|
42
59
|
return (
|
|
43
|
-
<
|
|
44
|
-
style={{ background: 'color-mix(in srgb, var(--success)
|
|
60
|
+
<span className="flex items-center gap-1.5 px-5 py-2 text-sm font-medium rounded-lg"
|
|
61
|
+
style={{ background: 'color-mix(in srgb, var(--success) 15%, transparent)', color: 'var(--success)' }}>
|
|
45
62
|
<CheckCircle2 size={14} /> {s.restartDone}
|
|
46
|
-
</
|
|
63
|
+
</span>
|
|
47
64
|
);
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
disabled={restarting}
|
|
61
|
-
className="flex items-center gap-1.5 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-50"
|
|
62
|
-
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
63
|
-
{restarting ? <Loader2 size={13} className="animate-spin" /> : null}
|
|
64
|
-
{restarting ? s.restarting : s.restartNow}
|
|
65
|
-
</button>
|
|
66
|
-
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
|
|
67
|
-
{s.restartManual} <code className="font-mono">mindos start</code>
|
|
68
|
-
</span>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={handleRestart}
|
|
71
|
+
disabled={restarting}
|
|
72
|
+
className="flex items-center gap-1.5 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
73
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
74
|
+
{restarting ? <Loader2 size={13} className="animate-spin" /> : null}
|
|
75
|
+
{restarting ? s.restarting : s.restartNow}
|
|
76
|
+
</button>
|
|
71
77
|
);
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -205,7 +211,7 @@ export default function StepReview({
|
|
|
205
211
|
{s.completeFailed}: {error}
|
|
206
212
|
</div>
|
|
207
213
|
)}
|
|
208
|
-
{needsRestart && setupPhase === 'done' && <
|
|
214
|
+
{needsRestart && setupPhase === 'done' && <RestartBanner s={s} />}
|
|
209
215
|
</div>
|
|
210
216
|
);
|
|
211
217
|
}
|
|
@@ -11,6 +11,7 @@ import StepPorts from './StepPorts';
|
|
|
11
11
|
import StepSecurity from './StepSecurity';
|
|
12
12
|
import StepAgents from './StepAgents';
|
|
13
13
|
import StepReview from './StepReview';
|
|
14
|
+
import { RestartButton } from './StepReview';
|
|
14
15
|
import StepDots from './StepDots';
|
|
15
16
|
|
|
16
17
|
// ─── Helpers (shared by handleComplete + retryAgent) ─────────────────────────
|
|
@@ -439,14 +440,16 @@ export default function SetupWizard() {
|
|
|
439
440
|
{s.next} <ChevronRight size={14} />
|
|
440
441
|
</button>
|
|
441
442
|
) : completed ? (
|
|
442
|
-
// After completing: show Done link
|
|
443
|
-
|
|
443
|
+
// After completing: show Done link or Restart button in the same position
|
|
444
|
+
needsRestart ? (
|
|
445
|
+
<RestartButton s={s} newPort={state.webPort} />
|
|
446
|
+
) : (
|
|
444
447
|
<a href="/?welcome=1"
|
|
445
448
|
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors"
|
|
446
449
|
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
447
450
|
{s.completeDone} →
|
|
448
451
|
</a>
|
|
449
|
-
)
|
|
452
|
+
)
|
|
450
453
|
) : (
|
|
451
454
|
<button
|
|
452
455
|
onClick={handleComplete}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function register() {
|
|
2
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
3
|
+
const { readFileSync } = await import('fs');
|
|
4
|
+
const { join, resolve } = await import('path');
|
|
5
|
+
const { homedir } = await import('os');
|
|
6
|
+
try {
|
|
7
|
+
const configPath = join(homedir(), '.mindos', 'config.json');
|
|
8
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
9
|
+
if (config.sync?.enabled && config.mindRoot) {
|
|
10
|
+
// Resolve absolute path to avoid Turbopack bundling issues
|
|
11
|
+
const syncModule = resolve(process.cwd(), '..', 'bin', 'lib', 'sync.js');
|
|
12
|
+
const { startSyncDaemon } = await import(/* webpackIgnore: true */ syncModule);
|
|
13
|
+
await startSyncDaemon(config.mindRoot);
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
// Sync not configured or failed to start — silently skip
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/app/lib/agent/prompt.ts
CHANGED
|
@@ -17,6 +17,38 @@ Tool policy:
|
|
|
17
17
|
- Prefer targeted edits (update_section / insert_after_heading / append_to_file) over full overwrite.
|
|
18
18
|
- Use write_file only when replacing the whole file is required.
|
|
19
19
|
- INSTRUCTION.md is read-only and must not be modified.
|
|
20
|
+
- Use append_csv for adding rows to CSV files instead of rewriting the whole file.
|
|
21
|
+
- Use get_backlinks before renaming/moving/deleting to understand impact on other files.
|
|
22
|
+
|
|
23
|
+
Destructive operations (use with caution):
|
|
24
|
+
- delete_file: permanently removes a file — cannot be undone
|
|
25
|
+
- move_file: changes file location — may break links in other files
|
|
26
|
+
- write_file: overwrites entire file content — prefer partial edits
|
|
27
|
+
Before executing destructive operations:
|
|
28
|
+
- Before delete_file: list what links to this file (get_backlinks), warn user about impact
|
|
29
|
+
- Before move_file: same — check backlinks first
|
|
30
|
+
- Before write_file (full overwrite): confirm with user that full replacement is intended
|
|
31
|
+
- NEVER chain multiple destructive operations without pausing to summarize what you've done
|
|
32
|
+
|
|
33
|
+
File management tools:
|
|
34
|
+
- rename_file: rename within same directory
|
|
35
|
+
- move_file: move to a different path (reports affected backlinks)
|
|
36
|
+
- get_backlinks: find all files that link to a given file
|
|
37
|
+
|
|
38
|
+
Git history tools:
|
|
39
|
+
- get_history: view commit log for a file
|
|
40
|
+
- get_file_at_version: read file content at a past commit (use get_history first to find hashes)
|
|
41
|
+
|
|
42
|
+
Complex task protocol:
|
|
43
|
+
1. PLAN: For multi-step tasks, first output a numbered plan
|
|
44
|
+
2. EXECUTE: Execute steps one by one, reporting progress
|
|
45
|
+
3. VERIFY: After edits, re-read the file to confirm correctness
|
|
46
|
+
4. SUMMARIZE: Conclude with a summary and suggest follow-up actions if relevant
|
|
47
|
+
|
|
48
|
+
Step awareness:
|
|
49
|
+
- You have a limited number of steps (configured by user, typically 10-30).
|
|
50
|
+
- If a tool call fails or returns unexpected results, do NOT retry with the same arguments.
|
|
51
|
+
- Try a different approach or ask the user for clarification.
|
|
20
52
|
|
|
21
53
|
Uploaded files:
|
|
22
54
|
- Users may upload local files (PDF, txt, csv, etc.) via the chat interface.
|