@geminilight/mindos 0.5.11 → 0.5.13
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 +191 -19
- package/app/app/api/mcp/install/route.ts +1 -1
- package/app/app/api/mcp/status/route.ts +11 -16
- package/app/app/api/settings/route.ts +3 -1
- package/app/app/api/setup/route.ts +7 -7
- package/app/app/api/sync/route.ts +18 -15
- package/app/components/AskModal.tsx +28 -32
- package/app/components/SettingsModal.tsx +7 -3
- package/app/components/ask/MessageList.tsx +65 -3
- package/app/components/ask/ThinkingBlock.tsx +55 -0
- package/app/components/ask/ToolCallBlock.tsx +97 -0
- package/app/components/settings/AiTab.tsx +76 -2
- package/app/components/settings/types.ts +8 -0
- package/app/components/setup/StepReview.tsx +31 -25
- package/app/components/setup/index.tsx +6 -3
- package/app/lib/agent/context.ts +317 -0
- package/app/lib/agent/index.ts +4 -0
- package/app/lib/agent/prompt.ts +46 -31
- package/app/lib/agent/stream-consumer.ts +212 -0
- package/app/lib/agent/tools.ts +159 -4
- package/app/lib/i18n.ts +28 -0
- package/app/lib/settings.ts +22 -0
- package/app/lib/types.ts +23 -0
- package/app/package.json +2 -3
- package/bin/cli.js +41 -21
- package/bin/lib/build.js +6 -2
- 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 +81 -40
- package/mcp/README.md +5 -5
- package/mcp/src/index.ts +2 -2
- package/package.json +3 -2
- package/scripts/setup.js +17 -12
- package/scripts/upgrade-prompt.md +6 -6
- package/skills/mindos/SKILL.md +47 -183
- package/skills/mindos-zh/SKILL.md +47 -183
- package/app/package-lock.json +0 -15615
|
@@ -11,6 +11,7 @@ import MessageList from '@/components/ask/MessageList';
|
|
|
11
11
|
import MentionPopover from '@/components/ask/MentionPopover';
|
|
12
12
|
import SessionHistory from '@/components/ask/SessionHistory';
|
|
13
13
|
import FileChip from '@/components/ask/FileChip';
|
|
14
|
+
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
14
15
|
|
|
15
16
|
interface AskModalProps {
|
|
16
17
|
open: boolean;
|
|
@@ -27,7 +28,6 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
27
28
|
const [isLoading, setIsLoading] = useState(false);
|
|
28
29
|
const [loadingPhase, setLoadingPhase] = useState<'connecting' | 'thinking' | 'streaming'>('connecting');
|
|
29
30
|
const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
|
|
30
|
-
const [maxSteps, setMaxSteps] = useState(20);
|
|
31
31
|
const [showHistory, setShowHistory] = useState(false);
|
|
32
32
|
|
|
33
33
|
const session = useAskSession(currentFile);
|
|
@@ -135,7 +135,6 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
135
135
|
currentFile,
|
|
136
136
|
attachedFiles,
|
|
137
137
|
uploadedFiles: upload.localAttachments,
|
|
138
|
-
maxSteps,
|
|
139
138
|
}),
|
|
140
139
|
signal: controller.signal,
|
|
141
140
|
});
|
|
@@ -151,25 +150,22 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
151
150
|
|
|
152
151
|
if (!res.body) throw new Error('No response body');
|
|
153
152
|
|
|
154
|
-
const reader = res.body.getReader();
|
|
155
|
-
const decoder = new TextDecoder();
|
|
156
|
-
let assistantContent = '';
|
|
157
153
|
setLoadingPhase('thinking');
|
|
158
154
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
const finalMessage = await consumeUIMessageStream(
|
|
156
|
+
res.body,
|
|
157
|
+
(msg) => {
|
|
158
|
+
setLoadingPhase('streaming');
|
|
159
|
+
session.setMessages(prev => {
|
|
160
|
+
const updated = [...prev];
|
|
161
|
+
updated[updated.length - 1] = msg;
|
|
162
|
+
return updated;
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
controller.signal,
|
|
166
|
+
);
|
|
171
167
|
|
|
172
|
-
if (!
|
|
168
|
+
if (!finalMessage.content.trim() && (!finalMessage.parts || finalMessage.parts.length === 0)) {
|
|
173
169
|
session.setMessages(prev => {
|
|
174
170
|
const updated = [...prev];
|
|
175
171
|
updated[updated.length - 1] = { role: 'assistant', content: `__error__${t.ask.errorNoResponse}` };
|
|
@@ -181,8 +177,12 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
181
177
|
session.setMessages(prev => {
|
|
182
178
|
const updated = [...prev];
|
|
183
179
|
const lastIdx = updated.length - 1;
|
|
184
|
-
if (lastIdx >= 0 && updated[lastIdx].role === 'assistant'
|
|
185
|
-
updated[lastIdx]
|
|
180
|
+
if (lastIdx >= 0 && updated[lastIdx].role === 'assistant') {
|
|
181
|
+
const last = updated[lastIdx];
|
|
182
|
+
const hasContent = last.content.trim() || (last.parts && last.parts.length > 0);
|
|
183
|
+
if (!hasContent) {
|
|
184
|
+
updated[lastIdx] = { role: 'assistant', content: `__error__${t.ask.stopped}` };
|
|
185
|
+
}
|
|
186
186
|
}
|
|
187
187
|
return updated;
|
|
188
188
|
});
|
|
@@ -191,9 +191,13 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
191
191
|
session.setMessages(prev => {
|
|
192
192
|
const updated = [...prev];
|
|
193
193
|
const lastIdx = updated.length - 1;
|
|
194
|
-
if (lastIdx >= 0 && updated[lastIdx].role === 'assistant'
|
|
195
|
-
updated[lastIdx]
|
|
196
|
-
|
|
194
|
+
if (lastIdx >= 0 && updated[lastIdx].role === 'assistant') {
|
|
195
|
+
const last = updated[lastIdx];
|
|
196
|
+
const hasContent = last.content.trim() || (last.parts && last.parts.length > 0);
|
|
197
|
+
if (!hasContent) {
|
|
198
|
+
updated[lastIdx] = { role: 'assistant', content: `__error__${errMsg}` };
|
|
199
|
+
return updated;
|
|
200
|
+
}
|
|
197
201
|
}
|
|
198
202
|
return [...updated, { role: 'assistant', content: `__error__${errMsg}` }];
|
|
199
203
|
});
|
|
@@ -202,7 +206,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
202
206
|
setIsLoading(false);
|
|
203
207
|
abortRef.current = null;
|
|
204
208
|
}
|
|
205
|
-
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery,
|
|
209
|
+
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, t.ask.errorNoResponse, t.ask.stopped]);
|
|
206
210
|
|
|
207
211
|
const handleResetSession = useCallback(() => {
|
|
208
212
|
if (isLoading) return;
|
|
@@ -382,14 +386,6 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
382
386
|
<div className="hidden md:flex px-4 pb-2 items-center gap-3 text-xs text-muted-foreground/50 shrink-0">
|
|
383
387
|
<span><kbd className="font-mono">↵</kbd> {t.ask.send}</span>
|
|
384
388
|
<span><kbd className="font-mono">@</kbd> {t.ask.attachFile}</span>
|
|
385
|
-
<span className="inline-flex items-center gap-1">
|
|
386
|
-
<span>Agent steps</span>
|
|
387
|
-
<select value={maxSteps} onChange={(e) => setMaxSteps(Number(e.target.value))} disabled={isLoading} className="bg-transparent border border-border rounded px-1.5 py-0.5 text-xs text-foreground">
|
|
388
|
-
<option value={10}>10</option>
|
|
389
|
-
<option value={20}>20</option>
|
|
390
|
-
<option value={30}>30</option>
|
|
391
|
-
</select>
|
|
392
|
-
</span>
|
|
393
389
|
<span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
|
|
394
390
|
</div>
|
|
395
391
|
</div>
|
|
@@ -6,7 +6,7 @@ import { useLocale } from '@/lib/LocaleContext';
|
|
|
6
6
|
import { getAllRenderers, loadDisabledState, isRendererEnabled } from '@/lib/renderers/registry';
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
8
8
|
import '@/lib/renderers/index';
|
|
9
|
-
import type { AiSettings, SettingsData, Tab } from './settings/types';
|
|
9
|
+
import type { AiSettings, AgentSettings, SettingsData, Tab } from './settings/types';
|
|
10
10
|
import { FONTS } from './settings/types';
|
|
11
11
|
import { AiTab } from './settings/AiTab';
|
|
12
12
|
import { AppearanceTab } from './settings/AppearanceTab';
|
|
@@ -88,7 +88,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
88
88
|
await apiFetch('/api/settings', {
|
|
89
89
|
method: 'POST',
|
|
90
90
|
headers: { 'Content-Type': 'application/json' },
|
|
91
|
-
body: JSON.stringify({ ai: data.ai, mindRoot: data.mindRoot, webPassword: data.webPassword, authToken: data.authToken }),
|
|
91
|
+
body: JSON.stringify({ ai: data.ai, agent: data.agent, mindRoot: data.mindRoot, webPassword: data.webPassword, authToken: data.authToken }),
|
|
92
92
|
});
|
|
93
93
|
setStatus('saved');
|
|
94
94
|
setTimeout(() => setStatus('idle'), 2500);
|
|
@@ -104,6 +104,10 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
104
104
|
setData(d => d ? { ...d, ai: { ...d.ai, ...patch } } : d);
|
|
105
105
|
}, []);
|
|
106
106
|
|
|
107
|
+
const updateAgent = useCallback((patch: Partial<AgentSettings>) => {
|
|
108
|
+
setData(d => d ? { ...d, agent: { ...(d.agent ?? {}), ...patch } } : d);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
107
111
|
const restoreFromEnv = useCallback(async () => {
|
|
108
112
|
if (!data) return;
|
|
109
113
|
const defaults: AiSettings = {
|
|
@@ -198,7 +202,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
198
202
|
</div>
|
|
199
203
|
) : (
|
|
200
204
|
<>
|
|
201
|
-
{tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} t={t} />}
|
|
205
|
+
{tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} updateAgent={updateAgent} t={t} />}
|
|
202
206
|
{tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
|
|
203
207
|
{tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
|
|
204
208
|
{tab === 'plugins' && <PluginsTab pluginStates={pluginStates} setPluginStates={setPluginStates} t={t} />}
|
|
@@ -1,10 +1,12 @@
|
|
|
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';
|
|
9
|
+
import ThinkingBlock from './ThinkingBlock';
|
|
8
10
|
|
|
9
11
|
function AssistantMessage({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
10
12
|
return (
|
|
@@ -28,6 +30,61 @@ function AssistantMessage({ content, isStreaming }: { content: string; isStreami
|
|
|
28
30
|
);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function AssistantMessageWithParts({ message, isStreaming }: { message: Message; isStreaming: boolean }) {
|
|
34
|
+
const parts = message.parts;
|
|
35
|
+
if (!parts || parts.length === 0) {
|
|
36
|
+
// Fallback to plain text rendering
|
|
37
|
+
return message.content ? (
|
|
38
|
+
<AssistantMessage content={message.content} isStreaming={isStreaming} />
|
|
39
|
+
) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if the last part is a running tool call — show a spinner after it
|
|
43
|
+
const lastPart = parts[parts.length - 1];
|
|
44
|
+
const showTrailingSpinner = isStreaming && lastPart.type === 'tool-call' && (lastPart.state === 'running' || lastPart.state === 'pending');
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
{parts.map((part, idx) => {
|
|
49
|
+
if (part.type === 'reasoning') {
|
|
50
|
+
const isLastPart = isStreaming && idx === parts.length - 1;
|
|
51
|
+
return <ThinkingBlock key={`reasoning-${idx}`} text={part.text} isStreaming={isLastPart} />;
|
|
52
|
+
}
|
|
53
|
+
if (part.type === 'text') {
|
|
54
|
+
const isLastTextPart = isStreaming && idx === parts.length - 1;
|
|
55
|
+
return part.text ? (
|
|
56
|
+
<AssistantMessage key={idx} content={part.text} isStreaming={isLastTextPart} />
|
|
57
|
+
) : null;
|
|
58
|
+
}
|
|
59
|
+
if (part.type === 'tool-call') {
|
|
60
|
+
return <ToolCallBlock key={part.toolCallId} part={part} />;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
})}
|
|
64
|
+
{showTrailingSpinner && (
|
|
65
|
+
<div className="flex items-center gap-2 py-1 mt-1">
|
|
66
|
+
<Loader2 size={12} className="animate-spin" style={{ color: 'var(--amber)' }} />
|
|
67
|
+
<span className="text-xs text-muted-foreground animate-pulse">Executing tool…</span>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function StepCounter({ parts }: { parts: Message['parts'] }) {
|
|
75
|
+
if (!parts) return null;
|
|
76
|
+
const toolCalls = parts.filter(p => p.type === 'tool-call');
|
|
77
|
+
if (toolCalls.length === 0) return null;
|
|
78
|
+
const lastToolCall = toolCalls[toolCalls.length - 1];
|
|
79
|
+
const toolLabel = lastToolCall.type === 'tool-call' ? lastToolCall.toolName : '';
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex items-center gap-1.5 mt-1.5 text-xs text-muted-foreground/70">
|
|
82
|
+
<Wrench size={10} />
|
|
83
|
+
<span>Step {toolCalls.length}{toolLabel ? ` — ${toolLabel}` : ''}</span>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
31
88
|
interface MessageListProps {
|
|
32
89
|
messages: Message[];
|
|
33
90
|
isLoading: boolean;
|
|
@@ -102,8 +159,13 @@ export default function MessageList({
|
|
|
102
159
|
</div>
|
|
103
160
|
) : (
|
|
104
161
|
<div className="max-w-[85%] px-3 py-2 rounded-xl rounded-bl-sm bg-muted text-foreground text-sm">
|
|
105
|
-
{m.content ? (
|
|
106
|
-
|
|
162
|
+
{(m.parts && m.parts.length > 0) || m.content ? (
|
|
163
|
+
<>
|
|
164
|
+
<AssistantMessageWithParts message={m} isStreaming={isLoading && i === messages.length - 1} />
|
|
165
|
+
{isLoading && i === messages.length - 1 && (
|
|
166
|
+
<StepCounter parts={m.parts} />
|
|
167
|
+
)}
|
|
168
|
+
</>
|
|
107
169
|
) : isLoading && i === messages.length - 1 ? (
|
|
108
170
|
<div className="flex items-center gap-2 py-1">
|
|
109
171
|
<Loader2 size={14} className="animate-spin" style={{ color: 'var(--amber)' }} />
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ChevronRight, ChevronDown, Brain } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
|
|
7
|
+
interface ThinkingBlockProps {
|
|
8
|
+
text: string;
|
|
9
|
+
isStreaming?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ThinkingBlock({ text, isStreaming }: ThinkingBlockProps) {
|
|
13
|
+
const [expanded, setExpanded] = useState(false);
|
|
14
|
+
const { t } = useLocale();
|
|
15
|
+
|
|
16
|
+
if (!text && !isStreaming) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="my-1 rounded-md border border-border/40 bg-muted/20 text-xs">
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
onClick={() => setExpanded(v => !v)}
|
|
23
|
+
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/30 transition-colors rounded-md"
|
|
24
|
+
>
|
|
25
|
+
{expanded ? (
|
|
26
|
+
<ChevronDown size={12} className="shrink-0 text-muted-foreground" />
|
|
27
|
+
) : (
|
|
28
|
+
<ChevronRight size={12} className="shrink-0 text-muted-foreground" />
|
|
29
|
+
)}
|
|
30
|
+
<Brain size={12} className="shrink-0 text-muted-foreground" />
|
|
31
|
+
<span className="text-muted-foreground font-medium">
|
|
32
|
+
{t.ask.thinkingLabel}
|
|
33
|
+
{isStreaming && !expanded && (
|
|
34
|
+
<span className="ml-1 animate-pulse">...</span>
|
|
35
|
+
)}
|
|
36
|
+
</span>
|
|
37
|
+
{!expanded && text && (
|
|
38
|
+
<span className="text-muted-foreground/60 truncate flex-1 ml-1">
|
|
39
|
+
{text.slice(0, 80)}{text.length > 80 ? '...' : ''}
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
</button>
|
|
43
|
+
{expanded && (
|
|
44
|
+
<div className="px-2 pb-2 pt-0.5 border-t border-border/30">
|
|
45
|
+
<div className="text-muted-foreground whitespace-pre-wrap leading-relaxed">
|
|
46
|
+
{text}
|
|
47
|
+
{isStreaming && (
|
|
48
|
+
<span className="inline-block w-1 h-3 bg-muted-foreground/40 ml-0.5 align-middle animate-pulse rounded-sm" />
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ChevronRight, ChevronDown, Loader2, CheckCircle2, XCircle, AlertTriangle } from 'lucide-react';
|
|
5
|
+
import type { ToolCallPart } from '@/lib/types';
|
|
6
|
+
|
|
7
|
+
const DESTRUCTIVE_TOOLS = new Set(['delete_file', 'move_file', 'rename_file', 'write_file']);
|
|
8
|
+
|
|
9
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
10
|
+
search: '🔍',
|
|
11
|
+
list_files: '📂',
|
|
12
|
+
read_file: '📖',
|
|
13
|
+
write_file: '✏️',
|
|
14
|
+
create_file: '📄',
|
|
15
|
+
append_to_file: '📝',
|
|
16
|
+
insert_after_heading: '📌',
|
|
17
|
+
update_section: '✏️',
|
|
18
|
+
delete_file: '🗑️',
|
|
19
|
+
rename_file: '📝',
|
|
20
|
+
move_file: '📦',
|
|
21
|
+
get_backlinks: '🔗',
|
|
22
|
+
get_history: '📜',
|
|
23
|
+
get_file_at_version: '⏪',
|
|
24
|
+
get_recent: '🕐',
|
|
25
|
+
append_csv: '📊',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function formatInput(input: unknown): string {
|
|
29
|
+
if (!input || typeof input !== 'object') return String(input ?? '');
|
|
30
|
+
const obj = input as Record<string, unknown>;
|
|
31
|
+
const parts: string[] = [];
|
|
32
|
+
for (const val of Object.values(obj)) {
|
|
33
|
+
if (typeof val === 'string') {
|
|
34
|
+
parts.push(val.length > 60 ? `${val.slice(0, 60)}…` : val);
|
|
35
|
+
} else if (Array.isArray(val)) {
|
|
36
|
+
parts.push(`[${val.length} items]`);
|
|
37
|
+
} else if (val !== undefined && val !== null) {
|
|
38
|
+
parts.push(String(val));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return parts.join(', ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function truncateOutput(output: string, maxLen = 200): string {
|
|
45
|
+
if (output.length <= maxLen) return output;
|
|
46
|
+
return output.slice(0, maxLen) + '…';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
|
|
50
|
+
const [expanded, setExpanded] = useState(false);
|
|
51
|
+
const icon = TOOL_ICONS[part.toolName] ?? '🔧';
|
|
52
|
+
const inputSummary = formatInput(part.input);
|
|
53
|
+
const isDestructive = DESTRUCTIVE_TOOLS.has(part.toolName);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={`my-1 rounded-md border text-xs font-mono ${
|
|
57
|
+
isDestructive
|
|
58
|
+
? 'border-amber-500/30 bg-amber-500/5'
|
|
59
|
+
: 'border-border/50 bg-muted/30'
|
|
60
|
+
}`}>
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => setExpanded(v => !v)}
|
|
64
|
+
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/50 transition-colors rounded-md"
|
|
65
|
+
>
|
|
66
|
+
{expanded ? <ChevronDown size={12} className="shrink-0 text-muted-foreground" /> : <ChevronRight size={12} className="shrink-0 text-muted-foreground" />}
|
|
67
|
+
{isDestructive && <AlertTriangle size={11} className="shrink-0 text-amber-500" />}
|
|
68
|
+
<span>{icon}</span>
|
|
69
|
+
<span className={`font-medium ${isDestructive ? 'text-amber-600 dark:text-amber-400' : 'text-foreground'}`}>{part.toolName}</span>
|
|
70
|
+
<span className="text-muted-foreground truncate flex-1">({inputSummary})</span>
|
|
71
|
+
<span className="shrink-0 ml-auto">
|
|
72
|
+
{part.state === 'pending' || part.state === 'running' ? (
|
|
73
|
+
<Loader2 size={12} className="animate-spin text-amber-500" />
|
|
74
|
+
) : part.state === 'done' ? (
|
|
75
|
+
<CheckCircle2 size={12} className="text-success" />
|
|
76
|
+
) : (
|
|
77
|
+
<XCircle size={12} className="text-error" />
|
|
78
|
+
)}
|
|
79
|
+
</span>
|
|
80
|
+
</button>
|
|
81
|
+
{expanded && (
|
|
82
|
+
<div className="px-2 pb-2 pt-0.5 border-t border-border/30 space-y-1">
|
|
83
|
+
<div className="text-muted-foreground">
|
|
84
|
+
<span className="font-semibold">Input: </span>
|
|
85
|
+
<span className="break-all whitespace-pre-wrap">{JSON.stringify(part.input, null, 2)}</span>
|
|
86
|
+
</div>
|
|
87
|
+
{part.output !== undefined && (
|
|
88
|
+
<div className="text-muted-foreground">
|
|
89
|
+
<span className="font-semibold">Output: </span>
|
|
90
|
+
<span className="break-all whitespace-pre-wrap">{truncateOutput(part.output)}</span>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
5
|
-
import type { AiSettings, ProviderConfig, SettingsData } from './types';
|
|
5
|
+
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData } from './types';
|
|
6
6
|
import { Field, Select, Input, EnvBadge, ApiKeyInput } from './Primitives';
|
|
7
7
|
|
|
8
8
|
type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
@@ -28,10 +28,11 @@ function errorMessage(t: any, code?: ErrorCode): string {
|
|
|
28
28
|
interface AiTabProps {
|
|
29
29
|
data: SettingsData;
|
|
30
30
|
updateAi: (patch: Partial<AiSettings>) => void;
|
|
31
|
+
updateAgent: (patch: Partial<AgentSettings>) => void;
|
|
31
32
|
t: any;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
35
|
+
export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
35
36
|
const env = data.envOverrides ?? {};
|
|
36
37
|
const envVal = data.envValues ?? {};
|
|
37
38
|
const provider = data.ai.provider;
|
|
@@ -224,6 +225,79 @@ export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
|
224
225
|
<span>{t.settings.ai.envHint}</span>
|
|
225
226
|
</div>
|
|
226
227
|
)}
|
|
228
|
+
|
|
229
|
+
{/* Agent Behavior */}
|
|
230
|
+
<div className="pt-3 border-t border-border">
|
|
231
|
+
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">{t.settings.agent.title}</h3>
|
|
232
|
+
|
|
233
|
+
<div className="space-y-4">
|
|
234
|
+
<Field label={t.settings.agent.maxSteps} hint={t.settings.agent.maxStepsHint}>
|
|
235
|
+
<Select
|
|
236
|
+
value={String(data.agent?.maxSteps ?? 20)}
|
|
237
|
+
onChange={e => updateAgent({ maxSteps: Number(e.target.value) })}
|
|
238
|
+
>
|
|
239
|
+
<option value="5">5</option>
|
|
240
|
+
<option value="10">10</option>
|
|
241
|
+
<option value="15">15</option>
|
|
242
|
+
<option value="20">20</option>
|
|
243
|
+
<option value="25">25</option>
|
|
244
|
+
<option value="30">30</option>
|
|
245
|
+
</Select>
|
|
246
|
+
</Field>
|
|
247
|
+
|
|
248
|
+
<Field label={t.settings.agent.contextStrategy} hint={t.settings.agent.contextStrategyHint}>
|
|
249
|
+
<Select
|
|
250
|
+
value={data.agent?.contextStrategy ?? 'auto'}
|
|
251
|
+
onChange={e => updateAgent({ contextStrategy: e.target.value as 'auto' | 'off' })}
|
|
252
|
+
>
|
|
253
|
+
<option value="auto">{t.settings.agent.contextStrategyAuto}</option>
|
|
254
|
+
<option value="off">{t.settings.agent.contextStrategyOff}</option>
|
|
255
|
+
</Select>
|
|
256
|
+
</Field>
|
|
257
|
+
|
|
258
|
+
{provider === 'anthropic' && (
|
|
259
|
+
<>
|
|
260
|
+
<div className="flex items-center justify-between">
|
|
261
|
+
<div>
|
|
262
|
+
<div className="text-sm text-foreground">{t.settings.agent.thinking}</div>
|
|
263
|
+
<div className="text-xs text-muted-foreground mt-0.5">{t.settings.agent.thinkingHint}</div>
|
|
264
|
+
</div>
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
role="switch"
|
|
268
|
+
aria-checked={data.agent?.enableThinking ?? false}
|
|
269
|
+
onClick={() => updateAgent({ enableThinking: !(data.agent?.enableThinking ?? false) })}
|
|
270
|
+
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
271
|
+
data.agent?.enableThinking ? 'bg-amber-500' : 'bg-muted'
|
|
272
|
+
}`}
|
|
273
|
+
>
|
|
274
|
+
<span
|
|
275
|
+
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
|
|
276
|
+
data.agent?.enableThinking ? 'translate-x-4' : 'translate-x-0'
|
|
277
|
+
}`}
|
|
278
|
+
/>
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{data.agent?.enableThinking && (
|
|
283
|
+
<Field label={t.settings.agent.thinkingBudget} hint={t.settings.agent.thinkingBudgetHint}>
|
|
284
|
+
<Input
|
|
285
|
+
type="number"
|
|
286
|
+
value={String(data.agent?.thinkingBudget ?? 5000)}
|
|
287
|
+
onChange={e => {
|
|
288
|
+
const v = parseInt(e.target.value, 10);
|
|
289
|
+
if (!isNaN(v)) updateAgent({ thinkingBudget: Math.max(1000, Math.min(50000, v)) });
|
|
290
|
+
}}
|
|
291
|
+
min={1000}
|
|
292
|
+
max={50000}
|
|
293
|
+
step={1000}
|
|
294
|
+
/>
|
|
295
|
+
</Field>
|
|
296
|
+
)}
|
|
297
|
+
</>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
227
301
|
</div>
|
|
228
302
|
);
|
|
229
303
|
}
|
|
@@ -14,8 +14,16 @@ export interface AiSettings {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface AgentSettings {
|
|
18
|
+
maxSteps?: number;
|
|
19
|
+
enableThinking?: boolean;
|
|
20
|
+
thinkingBudget?: number;
|
|
21
|
+
contextStrategy?: 'auto' | 'off';
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface SettingsData {
|
|
18
25
|
ai: AiSettings;
|
|
26
|
+
agent?: AgentSettings;
|
|
19
27
|
mindRoot: string;
|
|
20
28
|
webPassword?: string;
|
|
21
29
|
authToken?: string; // masked: first-xxxx-••••-last pattern
|
|
@@ -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}
|