@geminilight/mindos 0.6.21 → 0.6.23
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 +58 -46
- package/README_zh.md +58 -46
- package/app/app/api/file/import/route.ts +0 -2
- package/app/app/api/mcp/agents/route.ts +24 -0
- package/app/app/api/setup/route.ts +2 -0
- package/app/components/Breadcrumb.tsx +1 -1
- package/app/components/FileTree.tsx +18 -4
- package/app/components/HomeContent.tsx +1 -1
- package/app/components/RightAskPanel.tsx +17 -10
- package/app/components/SidebarLayout.tsx +4 -2
- package/app/components/agents/AgentsSkillsSection.tsx +1 -0
- package/app/components/agents/SkillDetailPopover.tsx +224 -69
- package/app/components/ask/AskContent.tsx +5 -5
- package/app/components/ask/FileChip.tsx +1 -1
- package/app/components/ask/MessageList.tsx +1 -1
- package/app/hooks/useAskPanel.ts +7 -3
- package/app/hooks/useFileImport.ts +1 -1
- package/app/lib/agent/tools.ts +1 -1
- package/app/lib/core/fs-ops.ts +3 -2
- package/app/lib/fs.ts +28 -11
- package/app/lib/i18n-en.ts +2 -0
- package/app/lib/i18n-zh.ts +2 -0
- package/app/lib/mcp-agents.ts +3 -3
- package/app/lib/settings.ts +1 -1
- package/bin/cli.js +38 -20
- package/bin/commands/ask.js +101 -0
- package/bin/commands/file.js +286 -0
- package/bin/commands/space.js +167 -0
- package/bin/commands/status.js +69 -0
- package/bin/lib/command.js +156 -0
- package/mcp/dist/index.cjs +1 -1
- package/mcp/src/index.ts +1 -1
- package/package.json +1 -1
- package/skills/mindos/SKILL.md +2 -2
- package/skills/mindos-zh/SKILL.md +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
BookOpen,
|
|
6
6
|
Code2,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Check,
|
|
9
9
|
FileText,
|
|
10
10
|
Loader2,
|
|
11
|
+
Plus,
|
|
11
12
|
Zap,
|
|
12
13
|
Search,
|
|
13
14
|
Server,
|
|
@@ -58,6 +59,7 @@ interface SkillDetailPopoverProps {
|
|
|
58
59
|
skillName: string | null;
|
|
59
60
|
skill?: SkillInfo | null;
|
|
60
61
|
agentNames?: string[];
|
|
62
|
+
allAgentNames?: string[];
|
|
61
63
|
isNative?: boolean;
|
|
62
64
|
nativeSourcePath?: string;
|
|
63
65
|
copy: SkillDetailPopoverCopy;
|
|
@@ -65,6 +67,8 @@ interface SkillDetailPopoverProps {
|
|
|
65
67
|
onToggle?: (name: string, enabled: boolean) => Promise<boolean>;
|
|
66
68
|
onDelete?: (name: string) => Promise<void>;
|
|
67
69
|
onRefresh?: () => Promise<void>;
|
|
70
|
+
onAddAgent?: (skillName: string, agentName: string) => void;
|
|
71
|
+
onRemoveAgent?: (skillName: string, agentName: string) => void;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/* ────────── Capability Icon ────────── */
|
|
@@ -77,6 +81,90 @@ const CAPABILITY_ICONS: Record<SkillCapability, React.ComponentType<{ size?: num
|
|
|
77
81
|
memory: BookOpen,
|
|
78
82
|
};
|
|
79
83
|
|
|
84
|
+
/* ────────── Content Parser ────────── */
|
|
85
|
+
|
|
86
|
+
interface ParsedContent {
|
|
87
|
+
triggerConditions: string;
|
|
88
|
+
instructions: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseSkillContent(raw: string, description: string): ParsedContent {
|
|
92
|
+
// Strip YAML frontmatter
|
|
93
|
+
let body = raw;
|
|
94
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
95
|
+
if (fmMatch) body = raw.slice(fmMatch[0].length).trim();
|
|
96
|
+
|
|
97
|
+
// Extract trigger conditions from description
|
|
98
|
+
// The description field usually contains trigger/usage info
|
|
99
|
+
const triggerConditions = description || '';
|
|
100
|
+
|
|
101
|
+
return { triggerConditions, instructions: body };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ────────── Simple Markdown Renderer ────────── */
|
|
105
|
+
|
|
106
|
+
function MarkdownContent({ text, className = '' }: { text: string; className?: string }) {
|
|
107
|
+
const html = useMemo(() => renderMarkdown(text), [text]);
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={`prose prose-sm prose-invert max-w-none
|
|
111
|
+
prose-headings:text-foreground prose-headings:font-semibold prose-headings:mt-4 prose-headings:mb-2
|
|
112
|
+
prose-h1:text-sm prose-h2:text-xs prose-h3:text-xs
|
|
113
|
+
prose-p:text-xs prose-p:text-foreground/80 prose-p:leading-relaxed prose-p:my-1.5
|
|
114
|
+
prose-li:text-xs prose-li:text-foreground/80 prose-li:my-0.5
|
|
115
|
+
prose-strong:text-foreground prose-strong:font-semibold
|
|
116
|
+
prose-code:text-[var(--amber)] prose-code:bg-muted/50 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-2xs prose-code:font-mono
|
|
117
|
+
prose-pre:bg-muted/30 prose-pre:border prose-pre:border-border/50 prose-pre:rounded-lg prose-pre:p-3 prose-pre:text-2xs prose-pre:overflow-x-auto
|
|
118
|
+
prose-ul:my-1 prose-ol:my-1
|
|
119
|
+
prose-hr:border-border/30 prose-hr:my-3
|
|
120
|
+
${className}`}
|
|
121
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Minimal markdown to HTML — no external deps */
|
|
127
|
+
function renderMarkdown(md: string): string {
|
|
128
|
+
let html = md
|
|
129
|
+
// Code blocks (fenced)
|
|
130
|
+
.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) =>
|
|
131
|
+
`<pre><code>${escHtml(code.trimEnd())}</code></pre>`)
|
|
132
|
+
// Inline code
|
|
133
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
134
|
+
// Headers
|
|
135
|
+
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
|
136
|
+
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
137
|
+
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
138
|
+
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
139
|
+
// Bold
|
|
140
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
141
|
+
// Italic
|
|
142
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
143
|
+
// HR
|
|
144
|
+
.replace(/^---$/gm, '<hr/>')
|
|
145
|
+
// List items
|
|
146
|
+
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
147
|
+
.replace(/^\* (.+)$/gm, '<li>$1</li>')
|
|
148
|
+
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
|
149
|
+
|
|
150
|
+
// Wrap consecutive <li> in <ul>
|
|
151
|
+
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
|
152
|
+
|
|
153
|
+
// Paragraphs: lines not wrapped in a block tag
|
|
154
|
+
html = html.split('\n').map(line => {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (!trimmed) return '';
|
|
157
|
+
if (/^<(h[1-4]|ul|ol|li|pre|hr|div|p|blockquote)/.test(trimmed)) return line;
|
|
158
|
+
return `<p>${trimmed}</p>`;
|
|
159
|
+
}).join('\n');
|
|
160
|
+
|
|
161
|
+
return html;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function escHtml(s: string): string {
|
|
165
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
166
|
+
}
|
|
167
|
+
|
|
80
168
|
/* ────────── Component ────────── */
|
|
81
169
|
|
|
82
170
|
export default function SkillDetailPopover({
|
|
@@ -84,6 +172,7 @@ export default function SkillDetailPopover({
|
|
|
84
172
|
skillName,
|
|
85
173
|
skill,
|
|
86
174
|
agentNames = [],
|
|
175
|
+
allAgentNames = [],
|
|
87
176
|
isNative = false,
|
|
88
177
|
nativeSourcePath,
|
|
89
178
|
copy,
|
|
@@ -91,6 +180,8 @@ export default function SkillDetailPopover({
|
|
|
91
180
|
onToggle,
|
|
92
181
|
onDelete,
|
|
93
182
|
onRefresh,
|
|
183
|
+
onAddAgent,
|
|
184
|
+
onRemoveAgent,
|
|
94
185
|
}: SkillDetailPopoverProps) {
|
|
95
186
|
const [content, setContent] = useState<string | null>(null);
|
|
96
187
|
const [nativeDesc, setNativeDesc] = useState<string>('');
|
|
@@ -101,7 +192,8 @@ export default function SkillDetailPopover({
|
|
|
101
192
|
const [deleting, setDeleting] = useState(false);
|
|
102
193
|
const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
|
|
103
194
|
const [toggleBusy, setToggleBusy] = useState(false);
|
|
104
|
-
const [
|
|
195
|
+
const [showAddAgent, setShowAddAgent] = useState(false);
|
|
196
|
+
const [contentExpanded, setContentExpanded] = useState(false);
|
|
105
197
|
|
|
106
198
|
const fetchContent = useCallback(async () => {
|
|
107
199
|
if (!skillName) return;
|
|
@@ -140,6 +232,8 @@ export default function SkillDetailPopover({
|
|
|
140
232
|
setDeleteMsg(null);
|
|
141
233
|
setDeleting(false);
|
|
142
234
|
setToggleBusy(false);
|
|
235
|
+
setShowAddAgent(false);
|
|
236
|
+
setContentExpanded(false);
|
|
143
237
|
fetchContent();
|
|
144
238
|
}
|
|
145
239
|
}, [open, skillName, fetchContent]);
|
|
@@ -202,6 +296,12 @@ export default function SkillDetailPopover({
|
|
|
202
296
|
const description = skill?.description || nativeDesc || '';
|
|
203
297
|
const skillPath = skill?.path || (isNative && nativeSourcePath ? `${nativeSourcePath}/${skillName}/SKILL.md` : '');
|
|
204
298
|
|
|
299
|
+
// Parse content into structured sections
|
|
300
|
+
const parsed = content ? parseSkillContent(content, description) : null;
|
|
301
|
+
|
|
302
|
+
// Available agents to add (not already assigned)
|
|
303
|
+
const availableAgents = allAgentNames.filter(a => !agentNames.includes(a));
|
|
304
|
+
|
|
205
305
|
return (
|
|
206
306
|
<>
|
|
207
307
|
{/* Backdrop */}
|
|
@@ -250,78 +350,103 @@ export default function SkillDetailPopover({
|
|
|
250
350
|
|
|
251
351
|
{/* ─── Body (scrollable) ─── */}
|
|
252
352
|
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
|
|
253
|
-
{/* Description */}
|
|
254
|
-
{description ? (
|
|
255
|
-
<div className="space-y-2">
|
|
256
|
-
<p className={`text-sm text-foreground leading-relaxed whitespace-pre-wrap ${!descriptionExpanded ? 'line-clamp-3' : ''}`}>
|
|
257
|
-
{description}
|
|
258
|
-
</p>
|
|
259
|
-
{description.split('\n').length > 3 && (
|
|
260
|
-
<button
|
|
261
|
-
type="button"
|
|
262
|
-
onClick={() => setDescriptionExpanded(!descriptionExpanded)}
|
|
263
|
-
className="inline-flex items-center gap-1 text-2xs font-medium text-[var(--amber)] hover:opacity-80 transition-opacity cursor-pointer"
|
|
264
|
-
>
|
|
265
|
-
<ChevronDown size={12} className={`transition-transform duration-200 ${descriptionExpanded ? 'rotate-180' : ''}`} />
|
|
266
|
-
<span>{descriptionExpanded ? '收起' : '查看全部'}</span>
|
|
267
|
-
</button>
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
) : !isNative ? (
|
|
271
|
-
<p className="text-sm text-muted-foreground italic">{copy.noDescription}</p>
|
|
272
|
-
) : null}
|
|
273
|
-
|
|
274
|
-
{/* Quick meta */}
|
|
275
|
-
<div className="grid grid-cols-2 gap-3">
|
|
276
|
-
{!isNative && skill && (
|
|
277
|
-
<MetaCard label={copy.enabled} value={skill.enabled ? '✓' : '—'} tone={skill.enabled ? 'ok' : 'muted'} />
|
|
278
|
-
)}
|
|
279
|
-
<MetaCard label={copy.capability} value={capability} />
|
|
280
|
-
<MetaCard label={copy.source} value={sourceLabel} />
|
|
281
|
-
<MetaCard label={copy.agents} value={String(agentNames.length)} />
|
|
282
|
-
</div>
|
|
283
353
|
|
|
284
|
-
{/*
|
|
285
|
-
{
|
|
286
|
-
<
|
|
287
|
-
<
|
|
288
|
-
<
|
|
289
|
-
|
|
354
|
+
{/* ── Section: Trigger Conditions ── */}
|
|
355
|
+
{description && (
|
|
356
|
+
<section>
|
|
357
|
+
<SectionTitle label="Trigger Conditions" />
|
|
358
|
+
<div className="rounded-lg border border-border/40 bg-muted/[0.03] p-3.5">
|
|
359
|
+
<p className="text-xs text-foreground/80 leading-relaxed whitespace-pre-wrap">
|
|
360
|
+
{description}
|
|
361
|
+
</p>
|
|
362
|
+
</div>
|
|
363
|
+
</section>
|
|
364
|
+
)}
|
|
365
|
+
{!description && !isNative && (
|
|
366
|
+
<p className="text-sm text-muted-foreground italic">{copy.noDescription}</p>
|
|
290
367
|
)}
|
|
291
368
|
|
|
292
|
-
{/*
|
|
293
|
-
|
|
294
|
-
<div>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
369
|
+
{/* ── Section: Quick Meta ── */}
|
|
370
|
+
<section>
|
|
371
|
+
<div className="grid grid-cols-2 gap-3">
|
|
372
|
+
{!isNative && skill && (
|
|
373
|
+
<MetaCard label={copy.enabled} value={skill.enabled ? '✓' : '—'} tone={skill.enabled ? 'ok' : 'muted'} />
|
|
374
|
+
)}
|
|
375
|
+
<MetaCard label={copy.capability} value={capability} />
|
|
376
|
+
<MetaCard label={copy.source} value={sourceLabel} />
|
|
377
|
+
<MetaCard label={copy.agents} value={String(agentNames.length)} />
|
|
301
378
|
</div>
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
379
|
+
</section>
|
|
380
|
+
|
|
381
|
+
{/* ── Section: Connected Agents ── */}
|
|
382
|
+
<section>
|
|
383
|
+
<div className="flex items-center justify-between mb-2">
|
|
384
|
+
<SectionTitle label={copy.agents} noMargin />
|
|
385
|
+
{onAddAgent && availableAgents.length > 0 && (
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
onClick={() => setShowAddAgent(!showAddAgent)}
|
|
389
|
+
className="inline-flex items-center gap-1 text-2xs font-medium text-[var(--amber)] hover:opacity-80 transition-opacity cursor-pointer"
|
|
390
|
+
>
|
|
391
|
+
<Plus size={12} />
|
|
392
|
+
<span>Add</span>
|
|
393
|
+
</button>
|
|
394
|
+
)}
|
|
306
395
|
</div>
|
|
307
|
-
)}
|
|
308
396
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
<h3 className="text-xs font-medium text-muted-foreground">{copy.content}</h3>
|
|
314
|
-
{content && (
|
|
397
|
+
{/* Add agent picker */}
|
|
398
|
+
{showAddAgent && availableAgents.length > 0 && (
|
|
399
|
+
<div className="flex flex-wrap gap-1.5 mb-2 p-2 rounded-lg border border-dashed border-[var(--amber)]/30 bg-[var(--amber)]/[0.03]">
|
|
400
|
+
{availableAgents.map((name) => (
|
|
315
401
|
<button
|
|
402
|
+
key={name}
|
|
316
403
|
type="button"
|
|
317
|
-
onClick={
|
|
318
|
-
className="inline-flex items-center gap-1
|
|
319
|
-
aria-label={copy.copyContent}
|
|
404
|
+
onClick={() => { onAddAgent?.(skillName, name); setShowAddAgent(false); }}
|
|
405
|
+
className="inline-flex items-center gap-1 px-2 py-1 text-2xs rounded-md border border-border bg-card hover:bg-muted cursor-pointer transition-colors"
|
|
320
406
|
>
|
|
321
|
-
|
|
322
|
-
{
|
|
407
|
+
<Plus size={10} className="text-[var(--amber)]" />
|
|
408
|
+
<span className="text-foreground/80">{name}</span>
|
|
323
409
|
</button>
|
|
324
|
-
)}
|
|
410
|
+
))}
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
|
|
414
|
+
{agentNames.length > 0 ? (
|
|
415
|
+
<div className="flex flex-wrap gap-2">
|
|
416
|
+
{agentNames.map((name) => (
|
|
417
|
+
<AgentAvatar
|
|
418
|
+
key={name}
|
|
419
|
+
name={name}
|
|
420
|
+
size="sm"
|
|
421
|
+
onRemove={onRemoveAgent ? () => onRemoveAgent(skillName, name) : undefined}
|
|
422
|
+
/>
|
|
423
|
+
))}
|
|
424
|
+
</div>
|
|
425
|
+
) : (
|
|
426
|
+
<div className="rounded-lg border border-dashed border-border p-3 text-center">
|
|
427
|
+
<p className="text-xs text-muted-foreground">{copy.noAgents}</p>
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
</section>
|
|
431
|
+
|
|
432
|
+
{/* ── Section: Skill Instructions (markdown) ── */}
|
|
433
|
+
{(content !== null || loading || loadError) && (
|
|
434
|
+
<section>
|
|
435
|
+
<div className="flex items-center justify-between mb-2">
|
|
436
|
+
<SectionTitle label="Instructions" noMargin />
|
|
437
|
+
<div className="flex items-center gap-2">
|
|
438
|
+
{content && (
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
onClick={handleCopy}
|
|
442
|
+
className="inline-flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1.5 py-0.5"
|
|
443
|
+
aria-label={copy.copyContent}
|
|
444
|
+
>
|
|
445
|
+
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
446
|
+
{copied ? copy.copied : copy.copyContent}
|
|
447
|
+
</button>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
325
450
|
</div>
|
|
326
451
|
|
|
327
452
|
{loading && (
|
|
@@ -344,12 +469,32 @@ export default function SkillDetailPopover({
|
|
|
344
469
|
</div>
|
|
345
470
|
)}
|
|
346
471
|
|
|
347
|
-
{
|
|
348
|
-
<
|
|
349
|
-
{
|
|
350
|
-
</
|
|
472
|
+
{parsed && !loading && !loadError && (
|
|
473
|
+
<div className={`rounded-lg border border-border/50 bg-muted/[0.02] p-4 overflow-hidden transition-all duration-200 ${!contentExpanded ? 'max-h-60' : ''}`}>
|
|
474
|
+
<MarkdownContent text={parsed.instructions} />
|
|
475
|
+
</div>
|
|
351
476
|
)}
|
|
352
|
-
|
|
477
|
+
{parsed && !loading && !loadError && parsed.instructions.split('\n').length > 15 && (
|
|
478
|
+
<button
|
|
479
|
+
type="button"
|
|
480
|
+
onClick={() => setContentExpanded(!contentExpanded)}
|
|
481
|
+
className="inline-flex items-center gap-1 mt-2 text-2xs font-medium text-[var(--amber)] hover:opacity-80 transition-opacity cursor-pointer"
|
|
482
|
+
>
|
|
483
|
+
<ChevronDown size={12} className={`transition-transform duration-200 ${contentExpanded ? 'rotate-180' : ''}`} />
|
|
484
|
+
<span>{contentExpanded ? 'Collapse' : 'View All'}</span>
|
|
485
|
+
</button>
|
|
486
|
+
)}
|
|
487
|
+
</section>
|
|
488
|
+
)}
|
|
489
|
+
|
|
490
|
+
{/* ── Section: File Path ── */}
|
|
491
|
+
{skillPath && (
|
|
492
|
+
<section>
|
|
493
|
+
<SectionTitle label={copy.path} />
|
|
494
|
+
<div className="rounded-lg border border-border/50 bg-muted/[0.03] p-3">
|
|
495
|
+
<code className="text-2xs text-foreground/60 font-mono break-all leading-relaxed">{skillPath}</code>
|
|
496
|
+
</div>
|
|
497
|
+
</section>
|
|
353
498
|
)}
|
|
354
499
|
|
|
355
500
|
{/* Delete message */}
|
|
@@ -410,6 +555,16 @@ export default function SkillDetailPopover({
|
|
|
410
555
|
);
|
|
411
556
|
}
|
|
412
557
|
|
|
558
|
+
/* ────────── Section Title ────────── */
|
|
559
|
+
|
|
560
|
+
function SectionTitle({ label, noMargin }: { label: string; noMargin?: boolean }) {
|
|
561
|
+
return (
|
|
562
|
+
<h3 className={`text-xs font-semibold text-muted-foreground uppercase tracking-wider ${noMargin ? '' : 'mb-2'}`}>
|
|
563
|
+
{label}
|
|
564
|
+
</h3>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
413
568
|
/* ────────── Meta Card ────────── */
|
|
414
569
|
|
|
415
570
|
function MetaCard({
|
|
@@ -601,24 +601,24 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
601
601
|
</span>
|
|
602
602
|
</div>
|
|
603
603
|
<div className="flex items-center gap-1">
|
|
604
|
-
<button type="button" onClick={() => setShowHistory(v => !v)} aria-pressed={showHistory} className={`p-1 rounded transition-colors ${showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title="Session history">
|
|
604
|
+
<button type="button" onClick={() => setShowHistory(v => !v)} aria-pressed={showHistory} className={`p-1.5 rounded transition-colors ${showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title="Session history">
|
|
605
605
|
<History size={iconSize} />
|
|
606
606
|
</button>
|
|
607
|
-
<button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title="New session">
|
|
607
|
+
<button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title="New session">
|
|
608
608
|
<RotateCcw size={iconSize} />
|
|
609
609
|
</button>
|
|
610
610
|
{isPanel && onMaximize && (
|
|
611
|
-
<button type="button" onClick={onMaximize} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? 'Restore panel' : 'Maximize panel'}>
|
|
611
|
+
<button type="button" onClick={onMaximize} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? 'Restore panel' : 'Maximize panel'}>
|
|
612
612
|
{maximized ? <Minimize2 size={iconSize} /> : <Maximize2 size={iconSize} />}
|
|
613
613
|
</button>
|
|
614
614
|
)}
|
|
615
615
|
{onModeSwitch && (
|
|
616
|
-
<button type="button" onClick={onModeSwitch} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? 'Dock to side panel' : 'Open as popup'}>
|
|
616
|
+
<button type="button" onClick={onModeSwitch} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? 'Dock to side panel' : 'Open as popup'}>
|
|
617
617
|
{askMode === 'popup' ? <PanelRight size={iconSize} /> : <AppWindow size={iconSize} />}
|
|
618
618
|
</button>
|
|
619
619
|
)}
|
|
620
620
|
{onClose && (
|
|
621
|
-
<button type="button" onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" aria-label="Close">
|
|
621
|
+
<button type="button" onClick={onClose} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" aria-label="Close">
|
|
622
622
|
<X size={isPanel ? iconSize : 15} />
|
|
623
623
|
</button>
|
|
624
624
|
)}
|
|
@@ -22,7 +22,7 @@ export default function FileChip({ path, onRemove, variant = 'kb' }: FileChipPro
|
|
|
22
22
|
type="button"
|
|
23
23
|
onClick={onRemove}
|
|
24
24
|
aria-label={`Remove ${name}`}
|
|
25
|
-
className="
|
|
25
|
+
className="p-1 -mr-1 rounded hover:bg-muted hover:text-foreground transition-colors shrink-0"
|
|
26
26
|
>
|
|
27
27
|
<X size={10} />
|
|
28
28
|
</button>
|
|
@@ -137,7 +137,7 @@ export default function MessageList({
|
|
|
137
137
|
}, [messages]);
|
|
138
138
|
|
|
139
139
|
return (
|
|
140
|
-
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4 min-h-0">
|
|
140
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-4 space-y-4 min-h-0">
|
|
141
141
|
{messages.length === 0 && (
|
|
142
142
|
<div className="mt-6 space-y-3">
|
|
143
143
|
<p className="text-center text-sm text-muted-foreground/60">{emptyPrompt}</p>
|
package/app/hooks/useAskPanel.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { useAskModal } from './useAskModal';
|
|
|
7
7
|
export interface AskPanelState {
|
|
8
8
|
askPanelOpen: boolean;
|
|
9
9
|
askPanelWidth: number;
|
|
10
|
+
askMaximized: boolean;
|
|
10
11
|
askMode: 'panel' | 'popup';
|
|
11
12
|
desktopAskPopupOpen: boolean;
|
|
12
13
|
askInitialMessage: string;
|
|
@@ -17,6 +18,7 @@ export interface AskPanelState {
|
|
|
17
18
|
handleAskWidthChange: (w: number) => void;
|
|
18
19
|
handleAskWidthCommit: (w: number) => void;
|
|
19
20
|
handleAskModeSwitch: () => void;
|
|
21
|
+
toggleAskMaximized: () => void;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -29,6 +31,7 @@ export function useAskPanel(): AskPanelState {
|
|
|
29
31
|
const [askMode, setAskMode] = useState<'panel' | 'popup'>('panel');
|
|
30
32
|
const [desktopAskPopupOpen, setDesktopAskPopupOpen] = useState(false);
|
|
31
33
|
const [askInitialMessage, setAskInitialMessage] = useState('');
|
|
34
|
+
const [askMaximized, setAskMaximized] = useState(false);
|
|
32
35
|
const [askOpenSource, setAskOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
|
|
33
36
|
|
|
34
37
|
const askModal = useAskModal();
|
|
@@ -82,7 +85,8 @@ export function useAskPanel(): AskPanelState {
|
|
|
82
85
|
}
|
|
83
86
|
}, [askMode]);
|
|
84
87
|
|
|
85
|
-
const closeAskPanel = useCallback(() => setAskPanelOpen(false), []);
|
|
88
|
+
const closeAskPanel = useCallback(() => { setAskPanelOpen(false); setAskMaximized(false); }, []);
|
|
89
|
+
const toggleAskMaximized = useCallback(() => setAskMaximized(v => !v), []);
|
|
86
90
|
const closeDesktopAskPopup = useCallback(() => setDesktopAskPopupOpen(false), []);
|
|
87
91
|
|
|
88
92
|
const handleAskWidthChange = useCallback((w: number) => setAskPanelWidth(w), []);
|
|
@@ -109,9 +113,9 @@ export function useAskPanel(): AskPanelState {
|
|
|
109
113
|
}, []);
|
|
110
114
|
|
|
111
115
|
return {
|
|
112
|
-
askPanelOpen, askPanelWidth, askMode, desktopAskPopupOpen,
|
|
116
|
+
askPanelOpen, askPanelWidth, askMaximized, askMode, desktopAskPopupOpen,
|
|
113
117
|
askInitialMessage, askOpenSource,
|
|
114
118
|
toggleAskPanel, closeAskPanel, closeDesktopAskPopup,
|
|
115
|
-
handleAskWidthChange, handleAskWidthCommit, handleAskModeSwitch,
|
|
119
|
+
handleAskWidthChange, handleAskWidthCommit, handleAskModeSwitch, toggleAskMaximized,
|
|
116
120
|
};
|
|
117
121
|
}
|
|
@@ -106,7 +106,7 @@ export function useFileImport() {
|
|
|
106
106
|
const merged = [...prev];
|
|
107
107
|
for (const f of newFiles) {
|
|
108
108
|
const isDup = merged.some(m =>
|
|
109
|
-
m.name === f.name && m.size === f.size
|
|
109
|
+
m.name === f.name && m.size === f.size && m.file.lastModified === f.file.lastModified
|
|
110
110
|
);
|
|
111
111
|
if (!isDup && merged.length < MAX_FILES) merged.push(f);
|
|
112
112
|
}
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -532,7 +532,7 @@ export const knowledgeBaseTools: AgentTool<any>[] = [
|
|
|
532
532
|
{
|
|
533
533
|
name: 'create_file',
|
|
534
534
|
label: 'Create File',
|
|
535
|
-
description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically.',
|
|
535
|
+
description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically. Does NOT create Space scaffolding (INSTRUCTION.md/README.md). Use create_space to create a Space.',
|
|
536
536
|
parameters: CreateFileParams,
|
|
537
537
|
execute: safeExecute(async (_id, params: Static<typeof CreateFileParams>) => {
|
|
538
538
|
createFile(params.path, params.content ?? '');
|
package/app/lib/core/fs-ops.ts
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { resolveSafe, assertWithinRoot } from './security';
|
|
4
4
|
import { MindOSError, ErrorCodes } from '@/lib/errors';
|
|
5
|
-
import {
|
|
5
|
+
import { cleanDirName, INSTRUCTION_TEMPLATE, README_TEMPLATE } from './space-scaffold';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Reads the content of a file given a relative path from mindRoot.
|
|
@@ -33,6 +33,8 @@ export function writeFile(mindRoot: string, filePath: string, content: string):
|
|
|
33
33
|
/**
|
|
34
34
|
* Creates a new file. Throws if the file already exists.
|
|
35
35
|
* Creates parent directories as needed.
|
|
36
|
+
* NOTE: Does NOT auto-scaffold Space files (INSTRUCTION.md/README.md).
|
|
37
|
+
* Use createSpaceFilesystem() or convertToSpace() for explicit Space creation.
|
|
36
38
|
*/
|
|
37
39
|
export function createFile(mindRoot: string, filePath: string, initialContent = ''): void {
|
|
38
40
|
const resolved = resolveSafe(mindRoot, filePath);
|
|
@@ -41,7 +43,6 @@ export function createFile(mindRoot: string, filePath: string, initialContent =
|
|
|
41
43
|
}
|
|
42
44
|
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
43
45
|
fs.writeFileSync(resolved, initialContent, 'utf-8');
|
|
44
|
-
scaffoldIfNewSpace(mindRoot, filePath);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
package/app/lib/fs.ts
CHANGED
|
@@ -59,9 +59,36 @@ const CACHE_TTL_MS = 5_000; // 5 seconds
|
|
|
59
59
|
|
|
60
60
|
let _treeVersion = 0;
|
|
61
61
|
|
|
62
|
+
function buildCache(root: string): FileTreeCache {
|
|
63
|
+
const tree = buildFileTree(root);
|
|
64
|
+
const allFiles: string[] = [];
|
|
65
|
+
function collect(nodes: FileNode[]) {
|
|
66
|
+
for (const n of nodes) {
|
|
67
|
+
if (n.type === 'file') allFiles.push(n.path);
|
|
68
|
+
else if (n.children) collect(n.children);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
collect(tree);
|
|
72
|
+
return { tree, allFiles, timestamp: Date.now() };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sameFileList(a: string[], b: string[]): boolean {
|
|
76
|
+
if (a.length !== b.length) return false;
|
|
77
|
+
const sa = [...a].sort();
|
|
78
|
+
const sb = [...b].sort();
|
|
79
|
+
return sa.every((p, i) => p === sb[i]);
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
/** Monotonically increasing counter — bumped on every file mutation so the
|
|
63
83
|
* client can cheaply detect changes without rebuilding the full tree. */
|
|
64
84
|
export function getTreeVersion(): number {
|
|
85
|
+
if (_cache && !isCacheValid()) {
|
|
86
|
+
const next = buildCache(getMindRoot());
|
|
87
|
+
const changed = !sameFileList(_cache.allFiles, next.allFiles);
|
|
88
|
+
_cache = next;
|
|
89
|
+
_searchIndex = null;
|
|
90
|
+
if (changed) _treeVersion++;
|
|
91
|
+
}
|
|
65
92
|
return _treeVersion;
|
|
66
93
|
}
|
|
67
94
|
|
|
@@ -80,17 +107,7 @@ export function invalidateCache(): void {
|
|
|
80
107
|
function ensureCache(): FileTreeCache {
|
|
81
108
|
if (isCacheValid()) return _cache!;
|
|
82
109
|
const root = getMindRoot();
|
|
83
|
-
|
|
84
|
-
// Extract all file paths from the tree to avoid a second full traversal.
|
|
85
|
-
const allFiles: string[] = [];
|
|
86
|
-
function collect(nodes: FileNode[]) {
|
|
87
|
-
for (const n of nodes) {
|
|
88
|
-
if (n.type === 'file') allFiles.push(n.path);
|
|
89
|
-
else if (n.children) collect(n.children);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
collect(tree);
|
|
93
|
-
_cache = { tree, allFiles, timestamp: Date.now() };
|
|
110
|
+
_cache = buildCache(root);
|
|
94
111
|
return _cache;
|
|
95
112
|
}
|
|
96
113
|
|
package/app/lib/i18n-en.ts
CHANGED
package/app/lib/i18n-zh.ts
CHANGED
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -494,7 +494,7 @@ export function detectAgentRuntimeSignals(agentKey: string): AgentRuntimeSignals
|
|
|
494
494
|
|
|
495
495
|
/* ── MindOS MCP Install Detection ──────────────────────────────────────── */
|
|
496
496
|
|
|
497
|
-
export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
|
|
497
|
+
export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string; url?: string } {
|
|
498
498
|
const agent = MCP_AGENTS[agentKey];
|
|
499
499
|
if (!agent) return { installed: false };
|
|
500
500
|
|
|
@@ -510,7 +510,7 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
|
|
|
510
510
|
if (result.found && result.entry) {
|
|
511
511
|
const entry = result.entry;
|
|
512
512
|
const transport = entry.type === 'stdio' ? 'stdio' : entry.url ? 'http' : 'unknown';
|
|
513
|
-
return { installed: true, scope: scopeType, transport, configPath: cfgPath };
|
|
513
|
+
return { installed: true, scope: scopeType, transport, configPath: cfgPath, url: entry.url };
|
|
514
514
|
}
|
|
515
515
|
} else {
|
|
516
516
|
// JSON format (default)
|
|
@@ -521,7 +521,7 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
|
|
|
521
521
|
if (servers?.mindos) {
|
|
522
522
|
const entry = servers.mindos as Record<string, unknown>;
|
|
523
523
|
const transport = entry.type === 'stdio' ? 'stdio' : entry.url ? 'http' : 'unknown';
|
|
524
|
-
return { installed: true, scope: scopeType, transport, configPath: cfgPath };
|
|
524
|
+
return { installed: true, scope: scopeType, transport, configPath: cfgPath, url: entry.url as string | undefined };
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
527
|
} catch { /* ignore parse errors */ }
|
package/app/lib/settings.ts
CHANGED
|
@@ -148,7 +148,7 @@ function parseGuideState(raw: unknown): GuideState | undefined {
|
|
|
148
148
|
askedAI: obj.askedAI === true,
|
|
149
149
|
nextStepIndex: typeof obj.nextStepIndex === 'number' ? obj.nextStepIndex : 0,
|
|
150
150
|
walkthroughStep: typeof obj.walkthroughStep === 'number' ? obj.walkthroughStep : undefined,
|
|
151
|
-
walkthroughDismissed: obj.walkthroughDismissed ===
|
|
151
|
+
walkthroughDismissed: typeof obj.walkthroughDismissed === 'boolean' ? obj.walkthroughDismissed : undefined,
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
|