merge: integrate upstream main with local Makelore changes

This commit is contained in:
inman
2026-08-31 10:55:48 +08:00
128 changed files with 8278 additions and 3030 deletions

View File

@@ -123,7 +123,7 @@ export function AgentCreationDialog({
setInput((current) => ({ ...current, avatarDataUrl: prepared.previewUrl }));
setAvatarPickerOpen(false);
} catch (error) {
toast.error(error instanceof Error ? error.message : '伙伴头像图片处理失败');
toast.error(error instanceof Error ? error.message : '智能体头像图片处理失败');
} finally {
setAvatarProcessing(false);
}
@@ -141,11 +141,11 @@ export function AgentCreationDialog({
const name = input.name.trim();
const responsibility = input.responsibility.trim();
if (!name || !responsibility || !input.model) {
toast.error('请填写伙伴名称职责,并配置伙伴模型。');
toast.error('请填写智能体名称职责说明,并配置默认模型。');
return;
}
if (existingAgentNames.some((agentName) => agentName.trim() === name)) {
toast.error('伙伴名称需要在当前项目内唯一。');
toast.error('智能体名称需要在当前项目内唯一。');
return;
}
const submit = editing ? onUpdate : onCreate;
@@ -165,35 +165,35 @@ export function AgentCreationDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[min(820px,calc(100vh-2rem))] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>{title ?? (editing ? '维护项目伙伴' : '创建项目伙伴')}</DialogTitle>
<DialogDescription></DialogDescription>
<DialogTitle>{title ?? (editing ? '编辑项目智能体' : '创建项目智能体')}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4 py-1">
<div>
<label htmlFor="agent-create-name" className="text-sm font-semibold"> <span className="text-destructive">*</span></label>
<label htmlFor="agent-create-name" className="text-sm font-semibold"> <span className="text-destructive">*</span></label>
<div className="mt-2 flex items-center gap-3">
<button type="button" aria-label="更换头像" title="更换头像" aria-haspopup="dialog" onClick={() => setAvatarPickerOpen(true)} className="motion-press h-10 w-10 shrink-0 overflow-hidden rounded-xl border border-brand bg-brand-soft p-0.5 shadow-soft">
<img src={selectedAvatarSrc} alt={input.avatarDataUrl ? '当前上传头像' : selectedAvatar?.label ?? '当前头像'} className="h-full w-full rounded-lg object-cover [image-rendering:pixelated]" />
</button>
<Input id="agent-create-name" value={input.name} maxLength={30} placeholder="例如:小明" onChange={(event) => setInput((current) => ({ ...current, name: event.target.value }))} className="mt-0 flex-1" />
<Input id="agent-create-name" value={input.name} maxLength={30} placeholder="例如:前端开发智能体" onChange={(event) => setInput((current) => ({ ...current, name: event.target.value }))} className="mt-0 flex-1" />
</div>
</div>
<div>
<label htmlFor="agent-create-model" className="text-sm font-semibold"> <span className="text-destructive">*</span></label>
<p className="mt-1 text-xs font-medium text-muted-foreground">使</p>
<label htmlFor="agent-create-model" className="text-sm font-semibold"> <span className="text-destructive">*</span></label>
<p className="mt-1 text-xs font-medium text-muted-foreground">使</p>
<select id="agent-create-model" value={input.model} onChange={(event) => setInput((current) => ({ ...current, model: event.target.value }))} className="mt-2 h-10 w-full rounded-xl border border-border bg-surface-input px-3 text-sm">
<option value="" disabled></option>
{modelOptions.map((option) => <option key={option.key} value={option.key}>{option.label}</option>)}
</select>
</div>
<div>
<label htmlFor="agent-create-responsibility" className="text-sm font-semibold"> <span className="text-destructive">*</span></label>
<label htmlFor="agent-create-responsibility" className="text-sm font-semibold"> <span className="text-destructive">*</span></label>
<Input id="agent-create-responsibility" value={input.responsibility} maxLength={200} placeholder="例如:负责把想法拆成清晰的产品计划。" onChange={(event) => setInput((current) => ({ ...current, responsibility: event.target.value }))} className="mt-2" />
</div>
<section className="space-y-4 rounded-lg border border-border/80 bg-surface-subtle p-4">
<h3 className="font-semibold"></h3>
<Textarea aria-label="Agent 提示词" value={input.prompt} onChange={(event) => setInput((current) => ({ ...current, prompt: event.target.value }))} className="mt-3 min-h-28 bg-background font-mono text-xs leading-5" placeholder="可选:补充这个伙伴的工作方式与边界。" />
<div className="flex items-center justify-between gap-3"><h4 className="text-sm font-semibold"></h4><Badge className="border border-border/80 bg-background text-foreground"> {input.skillIds.length}</Badge></div>
<h3 className="font-semibold"></h3>
<div><label htmlFor="agent-create-system-instructions" className="text-sm font-semibold"></label><Textarea id="agent-create-system-instructions" value={input.prompt} onChange={(event) => setInput((current) => ({ ...current, prompt: event.target.value }))} className="mt-2 min-h-28 bg-background font-mono text-xs leading-5" placeholder="可选:定义智能体的工作方式、约束和输出要求。" /></div>
<div className="flex items-center justify-between gap-3"><h4 className="text-sm font-semibold"></h4><Badge className="border border-border/80 bg-background text-foreground"> {input.skillIds.length}</Badge></div>
{skills.length > 0 ? <>
<div className="relative"><Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /><Input aria-label="搜索技能" value={skillQuery} onChange={(event) => setSkillQuery(event.target.value)} placeholder="搜索技能" className="bg-background pl-9" /></div>
<div className="space-y-2">{filteredSkills.map((skill) => { const checked = input.skillIds.includes(skill.id); const unavailable = skill.available === false; return <label key={skill.id} className={cn('flex items-start gap-2 rounded-md border border-border/80 p-2.5', unavailable ? 'cursor-not-allowed opacity-70' : 'cursor-pointer', checked ? 'bg-background' : 'bg-surface-subtle')}><input type="checkbox" aria-label={`绑定技能:${skill.name}`} checked={checked} disabled={unavailable} onChange={() => setInput((current) => ({ ...current, skillIds: checked ? current.skillIds.filter((id) => id !== skill.id) : [...current.skillIds, skill.id] }))} /><span><span className="text-sm font-semibold">{skill.name}</span><span className="mt-0.5 block text-xs text-muted-foreground">{skill.description}</span>{unavailable ? <span className="mt-1 block text-xs font-medium text-amber-700"></span> : null}</span></label>; })}{filteredSkills.length === 0 ? <p className="rounded-md border border-dashed border-border/80 p-3 text-center text-xs font-medium text-muted-foreground"></p> : null}</div>
@@ -201,10 +201,10 @@ export function AgentCreationDialog({
</section>
</div>
<DialogFooter className={cn(agent && onArchive && 'sm:justify-between')}>
{agent && onArchive ? <Button type="button" variant="outline" aria-label="归档伙伴" className="border-border/80 bg-accent-soft" onClick={onArchive} disabled={submitting}><Trash2 className="mr-2 h-4 w-4" /></Button> : null}
{agent && onArchive ? <Button type="button" variant="outline" aria-label="归档智能体" className="border-border/80 bg-accent-soft" onClick={onArchive} disabled={submitting}><Trash2 className="mr-2 h-4 w-4" /></Button> : null}
<div className="flex flex-col-reverse gap-2 sm:ml-auto sm:flex-row">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}></Button>
<Button type="button" onClick={() => void handleSubmit()} disabled={submitting || modelOptions.length === 0}>{submitting ? (editing ? '保存中…' : '创建中…') : (submitLabel ?? (editing ? '保存伙伴' : '创建并开始对话'))}</Button>
<Button type="button" onClick={() => void handleSubmit()} disabled={submitting || modelOptions.length === 0}>{submitting ? (editing ? '保存中…' : '创建中…') : (submitLabel ?? (editing ? '保存智能体' : '创建并开始对话'))}</Button>
</div>
</DialogFooter>
</DialogContent>
@@ -215,7 +215,7 @@ export function AgentCreationDialog({
ref={avatarUploadInputRef}
type="file"
accept="image/png,image/jpeg,image/webp"
aria-label="上传伙伴头像"
aria-label="上传智能体头像"
data-testid="agent-avatar-upload-input"
className="sr-only"
onChange={(event) => { void handleAvatarFileChange(event); }}
@@ -223,7 +223,7 @@ export function AgentCreationDialog({
<div className="flex flex-col gap-2 rounded-lg border border-dashed border-brand/35 bg-brand-soft/40 p-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-semibold"></p>
<p className="mt-1 text-xs font-medium text-muted-foreground"> 256×256 WebP</p>
<p className="mt-1 text-xs font-medium text-muted-foreground"> 256×256 WebP</p>
</div>
<Button type="button" variant="outline" className="shrink-0 border-brand/30 bg-background" onClick={() => avatarUploadInputRef.current?.click()} disabled={avatarProcessing}>
<Upload className="mr-2 h-4 w-4" />{avatarProcessing ? '处理中…' : '从本地上传'}

View File

@@ -18,7 +18,7 @@ export interface MonacoDiffViewerProps {
function diffModelPath(filePath: string, side: 'orig' | 'mod'): string {
const key = encodeURIComponent(filePath.replace(/\\/g, '/'));
return `inmemory://niancode-diff/${side}/${key}`;
return `inmemory://makelore-diff/${side}/${key}`;
}
export default function MonacoDiffViewer({
@@ -35,7 +35,7 @@ export default function MonacoDiffViewer({
return (
<div
data-testid="monaco-diff-viewer"
className={cn('niancode-diff-editor h-full w-full min-h-0', className)}
className={cn('makelore-diff-editor h-full w-full min-h-0', className)}
>
<DiffEditor
height="100%"

View File

@@ -1,3 +1,4 @@
import { ArrowLeft } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { AI_MODULE_SELECTION_PATH, aiModules, getAiModuleForPath } from '@/lib/ai-modules';
@@ -21,14 +22,19 @@ export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarC
title="返回首页"
onClick={() => navigate(AI_MODULE_SELECTION_PATH)}
className={cn(
'motion-press items-center bg-transparent text-left font-semibold text-foreground transition-colors duration-100 hover:bg-surface-subtle',
'motion-press group items-center bg-transparent text-left font-semibold text-foreground transition-colors duration-100 hover:bg-surface-subtle',
compactTrigger
? 'inline-flex min-h-9 w-fit translate-y-2 justify-start rounded-xl p-2 text-xs'
? 'inline-flex min-h-9 w-fit translate-y-2 justify-start gap-1.5 rounded-xl p-2 text-xs'
: 'flex min-h-11 w-full justify-between gap-2 rounded-2xl p-3 text-sm',
sidebarCollapsed ? 'justify-center px-0' : '',
)}
>
{!sidebarCollapsed ? <span className="min-w-0 whitespace-nowrap">{activeModuleLabel}</span> : null}
<ArrowLeft
data-testid="sidebar-module-return-icon"
aria-hidden="true"
className="h-4 w-4 shrink-0 -translate-y-px text-muted-foreground transition-[color,transform] duration-150 group-hover:-translate-x-0.5 group-hover:text-brand"
/>
{!sidebarCollapsed ? <span className="min-w-0 whitespace-nowrap leading-4">{activeModuleLabel}</span> : null}
</button>
</div>
);

View File

@@ -436,7 +436,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
const enterProject = async (project: CodingProjectSummary) => {
const result = await loadProjectConfig(project.id);
if (result.status !== 'valid' || !result.config) {
showProjectEntryError(project, result.status === 'missing' ? '项目缺少 .niancode/project.json' : result.error ?? '项目配置无效');
showProjectEntryError(project, result.status === 'missing' ? '项目缺少必要的配置文件' : result.error ?? '项目配置无效');
return;
}
await setActiveProject(project.id);
@@ -665,7 +665,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
)}
>
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{project.name}</span>
<div className="flex shrink-0 -space-x-1.5" aria-label="项目 Agent">
<div className="flex shrink-0 -space-x-1.5" aria-label="项目智能体">
{projectConfig?.agents.map((agent) => (
<span
key={agent.id}
@@ -1066,7 +1066,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
>
<h2 className="text-xl font-semibold"></h2>
<p className="mt-2 text-sm font-medium leading-6 text-muted-foreground">
.niancode/project.json
</p>
<pre className="mt-3 max-h-32 overflow-auto rounded-md border border-foreground/15 bg-white p-2 text-xs font-medium">
{projectEntryError.message}

View File

@@ -119,7 +119,7 @@ function ProductTitleBar({
overlay ? 'bg-transparent' : 'bg-background',
)}
style={{
right: overlay ? '0px' : 'var(--agent-browser-titlebar-logo-gap, 48px)',
right: '0px',
}}
>
<img