@geminilight/mindos 0.5.56 → 0.5.57
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/app/components/ask/AskContent.tsx +253 -65
- package/app/hooks/useComposerVerticalResize.ts +74 -0
- package/app/lib/i18n-en.ts +5 -0
- package/app/lib/i18n-zh.ts +5 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -0
- package/bin/cli.js +5 -1
- package/bin/lib/utils.js +6 -2
- package/mcp/package.json +1 -1
- package/package.json +2 -1
- package/scripts/release.sh +7 -0
- package/scripts/verify-standalone.mjs +129 -0
- package/skills/mindos/SKILL.md +13 -1
- package/skills/mindos-zh/SKILL.md +13 -1
|
@@ -13,6 +13,33 @@ import SessionHistory from '@/components/ask/SessionHistory';
|
|
|
13
13
|
import FileChip from '@/components/ask/FileChip';
|
|
14
14
|
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
15
15
|
import { cn } from '@/lib/utils';
|
|
16
|
+
import { useComposerVerticalResize } from '@/hooks/useComposerVerticalResize';
|
|
17
|
+
|
|
18
|
+
const PANEL_COMPOSER_STORAGE = 'mindos-agent-panel-composer-height';
|
|
19
|
+
const PANEL_COMPOSER_DEFAULT = 104;
|
|
20
|
+
const PANEL_COMPOSER_MIN = 84;
|
|
21
|
+
const PANEL_COMPOSER_MAX_ABS = 440;
|
|
22
|
+
const PANEL_COMPOSER_MAX_VIEW = 0.48;
|
|
23
|
+
const PANEL_COMPOSER_KEY_STEP = 24;
|
|
24
|
+
|
|
25
|
+
function panelComposerMaxForViewport(): number {
|
|
26
|
+
if (typeof window === 'undefined') return PANEL_COMPOSER_MAX_ABS;
|
|
27
|
+
return Math.min(PANEL_COMPOSER_MAX_ABS, Math.floor(window.innerHeight * PANEL_COMPOSER_MAX_VIEW));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readStoredPanelComposerHeight(): number {
|
|
31
|
+
if (typeof window === 'undefined') return PANEL_COMPOSER_DEFAULT;
|
|
32
|
+
try {
|
|
33
|
+
const s = localStorage.getItem(PANEL_COMPOSER_STORAGE);
|
|
34
|
+
if (s) {
|
|
35
|
+
const n = parseInt(s, 10);
|
|
36
|
+
if (Number.isFinite(n) && n >= PANEL_COMPOSER_MIN && n <= PANEL_COMPOSER_MAX_ABS) return n;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
41
|
+
return PANEL_COMPOSER_DEFAULT;
|
|
42
|
+
}
|
|
16
43
|
|
|
17
44
|
interface AskContentProps {
|
|
18
45
|
/** Controls visibility — 'open' for modal, 'active' for panel */
|
|
@@ -33,11 +60,79 @@ interface AskContentProps {
|
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
export default function AskContent({ visible, currentFile, initialMessage, onFirstMessage, variant, onClose, maximized, onMaximize, askMode, onModeSwitch }: AskContentProps) {
|
|
36
|
-
const
|
|
63
|
+
const isPanel = variant === 'panel';
|
|
64
|
+
|
|
65
|
+
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
|
37
66
|
const abortRef = useRef<AbortController | null>(null);
|
|
38
67
|
const firstMessageFired = useRef(false);
|
|
39
68
|
const { t } = useLocale();
|
|
40
69
|
|
|
70
|
+
const [panelComposerHeight, setPanelComposerHeight] = useState(readStoredPanelComposerHeight);
|
|
71
|
+
const panelComposerHRef = useRef(panelComposerHeight);
|
|
72
|
+
panelComposerHRef.current = panelComposerHeight;
|
|
73
|
+
|
|
74
|
+
const getPanelComposerHeight = useCallback(() => panelComposerHRef.current, []);
|
|
75
|
+
const persistPanelComposerHeight = useCallback((h: number) => {
|
|
76
|
+
try {
|
|
77
|
+
localStorage.setItem(PANEL_COMPOSER_STORAGE, String(h));
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const onPanelComposerResizePointerDown = useComposerVerticalResize({
|
|
84
|
+
minHeight: PANEL_COMPOSER_MIN,
|
|
85
|
+
maxHeightAbs: PANEL_COMPOSER_MAX_ABS,
|
|
86
|
+
maxHeightViewportRatio: PANEL_COMPOSER_MAX_VIEW,
|
|
87
|
+
getHeight: getPanelComposerHeight,
|
|
88
|
+
setHeight: setPanelComposerHeight,
|
|
89
|
+
persist: persistPanelComposerHeight,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const [panelComposerViewportMax, setPanelComposerViewportMax] = useState(panelComposerMaxForViewport);
|
|
93
|
+
|
|
94
|
+
const applyPanelComposerClampAndPersist = useCallback(() => {
|
|
95
|
+
const maxH = panelComposerMaxForViewport();
|
|
96
|
+
setPanelComposerViewportMax(maxH);
|
|
97
|
+
const h = panelComposerHRef.current;
|
|
98
|
+
if (h > maxH) {
|
|
99
|
+
setPanelComposerHeight(maxH);
|
|
100
|
+
panelComposerHRef.current = maxH;
|
|
101
|
+
persistPanelComposerHeight(maxH);
|
|
102
|
+
}
|
|
103
|
+
}, [persistPanelComposerHeight]);
|
|
104
|
+
|
|
105
|
+
const handlePanelComposerSeparatorKeyDown = useCallback(
|
|
106
|
+
(e: React.KeyboardEvent<HTMLElement>) => {
|
|
107
|
+
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) return;
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
const maxH = panelComposerMaxForViewport();
|
|
110
|
+
setPanelComposerViewportMax(maxH);
|
|
111
|
+
const h = panelComposerHRef.current;
|
|
112
|
+
let next = h;
|
|
113
|
+
if (e.key === 'ArrowUp') next = h + PANEL_COMPOSER_KEY_STEP;
|
|
114
|
+
else if (e.key === 'ArrowDown') next = h - PANEL_COMPOSER_KEY_STEP;
|
|
115
|
+
else if (e.key === 'Home') next = PANEL_COMPOSER_MIN;
|
|
116
|
+
else if (e.key === 'End') next = maxH;
|
|
117
|
+
const clamped = Math.round(Math.max(PANEL_COMPOSER_MIN, Math.min(maxH, next)));
|
|
118
|
+
setPanelComposerHeight(clamped);
|
|
119
|
+
panelComposerHRef.current = clamped;
|
|
120
|
+
persistPanelComposerHeight(clamped);
|
|
121
|
+
},
|
|
122
|
+
[persistPanelComposerHeight],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const resetPanelComposerHeight = useCallback(
|
|
126
|
+
(e: React.MouseEvent) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
setPanelComposerHeight(PANEL_COMPOSER_DEFAULT);
|
|
130
|
+
panelComposerHRef.current = PANEL_COMPOSER_DEFAULT;
|
|
131
|
+
persistPanelComposerHeight(PANEL_COMPOSER_DEFAULT);
|
|
132
|
+
},
|
|
133
|
+
[persistPanelComposerHeight],
|
|
134
|
+
);
|
|
135
|
+
|
|
41
136
|
const [input, setInput] = useState('');
|
|
42
137
|
const [isLoading, setIsLoading] = useState(false);
|
|
43
138
|
const [loadingPhase, setLoadingPhase] = useState<'connecting' | 'thinking' | 'streaming'>('connecting');
|
|
@@ -93,6 +188,13 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
93
188
|
return () => window.removeEventListener('keydown', handler);
|
|
94
189
|
}, [variant, visible, onClose, mention]);
|
|
95
190
|
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!isPanel) return;
|
|
193
|
+
applyPanelComposerClampAndPersist();
|
|
194
|
+
window.addEventListener('resize', applyPanelComposerClampAndPersist);
|
|
195
|
+
return () => window.removeEventListener('resize', applyPanelComposerClampAndPersist);
|
|
196
|
+
}, [isPanel, applyPanelComposerClampAndPersist]);
|
|
197
|
+
|
|
96
198
|
const handleInputChange = useCallback((val: string) => {
|
|
97
199
|
setInput(val);
|
|
98
200
|
mention.updateMentionFromInput(val);
|
|
@@ -108,21 +210,32 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
108
210
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
109
211
|
}, [input, attachedFiles, mention]);
|
|
110
212
|
|
|
111
|
-
const handleInputKeyDown = useCallback(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
213
|
+
const handleInputKeyDown = useCallback(
|
|
214
|
+
(e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
215
|
+
if (mention.mentionQuery !== null) {
|
|
216
|
+
if (e.key === 'ArrowDown') {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
mention.navigateMention('down');
|
|
219
|
+
} else if (e.key === 'ArrowUp') {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
mention.navigateMention('up');
|
|
222
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
223
|
+
if (e.key === 'Enter' && e.shiftKey) return;
|
|
224
|
+
if (mention.mentionResults.length > 0) {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
selectMention(mention.mentionResults[mention.mentionIndex]);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Panel: multiline input — Enter sends, Shift+Enter inserts newline (textarea default).
|
|
232
|
+
if (variant === 'panel' && e.key === 'Enter' && !e.shiftKey && !isLoading && input.trim()) {
|
|
121
233
|
e.preventDefault();
|
|
122
|
-
|
|
234
|
+
(e.currentTarget as HTMLTextAreaElement).form?.requestSubmit();
|
|
123
235
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
236
|
+
},
|
|
237
|
+
[mention, selectMention, variant, isLoading, input],
|
|
238
|
+
);
|
|
126
239
|
|
|
127
240
|
const handleStop = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
128
241
|
|
|
@@ -250,7 +363,6 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
250
363
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
251
364
|
}, [session, currentFile, upload, mention]);
|
|
252
365
|
|
|
253
|
-
const isPanel = variant === 'panel';
|
|
254
366
|
const iconSize = isPanel ? 13 : 14;
|
|
255
367
|
const inputIconSize = isPanel ? 14 : 15;
|
|
256
368
|
|
|
@@ -319,47 +431,79 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
319
431
|
labels={{ connecting: t.ask.connecting, thinking: t.ask.thinking, generating: t.ask.generating }}
|
|
320
432
|
/>
|
|
321
433
|
|
|
322
|
-
{/* Input area */}
|
|
323
|
-
<div
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
434
|
+
{/* Input area — panel: fixed-height shell + top drag handle (persisted); modal: simple block */}
|
|
435
|
+
<div
|
|
436
|
+
className={cn('shrink-0 border-t border-border', isPanel && 'flex flex-col overflow-hidden bg-card')}
|
|
437
|
+
style={isPanel ? { height: panelComposerHeight } : undefined}
|
|
438
|
+
>
|
|
439
|
+
{isPanel ? (
|
|
440
|
+
<div
|
|
441
|
+
role="separator"
|
|
442
|
+
tabIndex={0}
|
|
443
|
+
aria-orientation="horizontal"
|
|
444
|
+
aria-label={`${t.ask.panelComposerResize}. ${t.ask.panelComposerResetHint}. ${t.ask.panelComposerKeyboard}`}
|
|
445
|
+
aria-valuemin={PANEL_COMPOSER_MIN}
|
|
446
|
+
aria-valuemax={panelComposerViewportMax}
|
|
447
|
+
aria-valuenow={panelComposerHeight}
|
|
448
|
+
title={`${t.ask.panelComposerResize} · ${t.ask.panelComposerResetHint} · ${t.ask.panelComposerKeyboard}`}
|
|
449
|
+
onPointerDown={onPanelComposerResizePointerDown}
|
|
450
|
+
onKeyDown={handlePanelComposerSeparatorKeyDown}
|
|
451
|
+
onDoubleClick={resetPanelComposerHeight}
|
|
452
|
+
className="group flex h-3 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-muted/[0.06] transition-colors hover:bg-muted/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
|
453
|
+
>
|
|
454
|
+
<span
|
|
455
|
+
className="pointer-events-none h-1 w-10 rounded-full bg-border transition-colors group-hover:bg-[var(--amber)]/45 group-active:bg-[var(--amber)]/60"
|
|
456
|
+
aria-hidden
|
|
457
|
+
/>
|
|
334
458
|
</div>
|
|
335
|
-
)}
|
|
336
|
-
|
|
337
|
-
{
|
|
338
|
-
|
|
339
|
-
<div className={
|
|
340
|
-
{isPanel ? '
|
|
459
|
+
) : null}
|
|
460
|
+
|
|
461
|
+
<div className={cn(isPanel && 'flex min-h-0 flex-1 flex-col overflow-hidden')}>
|
|
462
|
+
{attachedFiles.length > 0 && (
|
|
463
|
+
<div className={cn('shrink-0', isPanel ? 'px-3 pt-2 pb-1' : 'px-4 pt-2.5 pb-1')}>
|
|
464
|
+
<div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
|
|
465
|
+
{isPanel ? 'Context' : 'Knowledge Base Context'}
|
|
466
|
+
</div>
|
|
467
|
+
<div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
|
|
468
|
+
{attachedFiles.map(f => (
|
|
469
|
+
<FileChip key={f} path={f} onRemove={() => setAttachedFiles(prev => prev.filter(x => x !== f))} />
|
|
470
|
+
))}
|
|
471
|
+
</div>
|
|
341
472
|
</div>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
473
|
+
)}
|
|
474
|
+
|
|
475
|
+
{upload.localAttachments.length > 0 && (
|
|
476
|
+
<div className={cn('shrink-0', isPanel ? 'px-3 pb-1' : 'px-4 pb-1')}>
|
|
477
|
+
<div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
|
|
478
|
+
{isPanel ? 'Uploaded' : 'Uploaded Files'}
|
|
479
|
+
</div>
|
|
480
|
+
<div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
|
|
481
|
+
{upload.localAttachments.map((f, idx) => (
|
|
482
|
+
<FileChip key={`${f.name}-${idx}`} path={f.name} variant="upload" onRemove={() => upload.removeAttachment(idx)} />
|
|
483
|
+
))}
|
|
484
|
+
</div>
|
|
346
485
|
</div>
|
|
347
|
-
|
|
348
|
-
)}
|
|
486
|
+
)}
|
|
349
487
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
488
|
+
{upload.uploadError && (
|
|
489
|
+
<div className={cn('shrink-0 pb-1 text-xs text-error', isPanel ? 'px-3' : 'px-4')}>{upload.uploadError}</div>
|
|
490
|
+
)}
|
|
353
491
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
492
|
+
{mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
|
|
493
|
+
<MentionPopover
|
|
494
|
+
results={mention.mentionResults}
|
|
495
|
+
selectedIndex={mention.mentionIndex}
|
|
496
|
+
onSelect={selectMention}
|
|
497
|
+
/>
|
|
498
|
+
)}
|
|
361
499
|
|
|
362
|
-
|
|
500
|
+
<form
|
|
501
|
+
onSubmit={handleSubmit}
|
|
502
|
+
className={cn(
|
|
503
|
+
'flex',
|
|
504
|
+
isPanel ? 'min-h-0 flex-1 items-end gap-1.5 px-2 py-2' : 'items-center gap-2 px-3 py-3',
|
|
505
|
+
)}
|
|
506
|
+
>
|
|
363
507
|
<button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title="Attach local file">
|
|
364
508
|
<Paperclip size={inputIconSize} />
|
|
365
509
|
</button>
|
|
@@ -385,7 +529,10 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
385
529
|
const pos = el.selectionStart ?? input.length;
|
|
386
530
|
const newVal = input.slice(0, pos) + '@' + input.slice(pos);
|
|
387
531
|
handleInputChange(newVal);
|
|
388
|
-
setTimeout(() => {
|
|
532
|
+
setTimeout(() => {
|
|
533
|
+
el.focus();
|
|
534
|
+
el.setSelectionRange(pos + 1, pos + 1);
|
|
535
|
+
}, 0);
|
|
389
536
|
}}
|
|
390
537
|
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
|
|
391
538
|
title="@ mention file"
|
|
@@ -393,15 +540,32 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
393
540
|
<AtSign size={inputIconSize} />
|
|
394
541
|
</button>
|
|
395
542
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
543
|
+
{isPanel ? (
|
|
544
|
+
<textarea
|
|
545
|
+
ref={(el) => {
|
|
546
|
+
inputRef.current = el;
|
|
547
|
+
}}
|
|
548
|
+
value={input}
|
|
549
|
+
onChange={e => handleInputChange(e.target.value)}
|
|
550
|
+
onKeyDown={handleInputKeyDown}
|
|
551
|
+
placeholder={t.ask.placeholder}
|
|
552
|
+
disabled={isLoading}
|
|
553
|
+
rows={1}
|
|
554
|
+
className="min-h-0 min-w-0 flex-1 resize-none overflow-y-auto bg-transparent py-2 text-sm leading-snug text-foreground placeholder:text-muted-foreground outline-none transition-[height] duration-75 disabled:opacity-50"
|
|
555
|
+
/>
|
|
556
|
+
) : (
|
|
557
|
+
<input
|
|
558
|
+
ref={(el) => {
|
|
559
|
+
inputRef.current = el;
|
|
560
|
+
}}
|
|
561
|
+
value={input}
|
|
562
|
+
onChange={e => handleInputChange(e.target.value)}
|
|
563
|
+
onKeyDown={handleInputKeyDown}
|
|
564
|
+
placeholder={t.ask.placeholder}
|
|
565
|
+
disabled={isLoading}
|
|
566
|
+
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50 min-w-0"
|
|
567
|
+
/>
|
|
568
|
+
)}
|
|
405
569
|
|
|
406
570
|
{isLoading ? (
|
|
407
571
|
<button type="button" onClick={handleStop} className="p-1.5 rounded-md transition-colors shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted" title={t.ask.stopTitle}>
|
|
@@ -412,7 +576,8 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
412
576
|
<Send size={isPanel ? 13 : 14} />
|
|
413
577
|
</button>
|
|
414
578
|
)}
|
|
415
|
-
|
|
579
|
+
</form>
|
|
580
|
+
</div>
|
|
416
581
|
</div>
|
|
417
582
|
|
|
418
583
|
{/* Footer hints — use full class strings so Tailwind JIT includes utilities */}
|
|
@@ -420,13 +585,36 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
420
585
|
className={cn(
|
|
421
586
|
'flex shrink-0 items-center',
|
|
422
587
|
isPanel
|
|
423
|
-
? 'gap-2 px-3 pb-1.5 text-[10px] text-muted-foreground/40'
|
|
588
|
+
? 'flex-wrap gap-x-2 gap-y-1 px-3 pb-1.5 text-[10px] text-muted-foreground/40'
|
|
424
589
|
: 'hidden gap-3 px-4 pb-2 text-xs text-muted-foreground/50 md:flex',
|
|
425
590
|
)}
|
|
426
591
|
>
|
|
427
|
-
<span
|
|
428
|
-
|
|
429
|
-
|
|
592
|
+
<span suppressHydrationWarning>
|
|
593
|
+
<kbd className="font-mono">↵</kbd> {t.ask.send}
|
|
594
|
+
</span>
|
|
595
|
+
{isPanel ? (
|
|
596
|
+
<span suppressHydrationWarning>
|
|
597
|
+
<kbd className="font-mono">⇧</kbd>
|
|
598
|
+
<kbd className="font-mono ml-0.5">↵</kbd> {t.ask.newlineHint}
|
|
599
|
+
</span>
|
|
600
|
+
) : null}
|
|
601
|
+
{isPanel ? (
|
|
602
|
+
<span
|
|
603
|
+
className="hidden sm:inline"
|
|
604
|
+
suppressHydrationWarning
|
|
605
|
+
title={`${t.ask.panelComposerResize} · ${t.ask.panelComposerResetHint} · ${t.ask.panelComposerKeyboard}`}
|
|
606
|
+
>
|
|
607
|
+
<kbd className="font-mono">↕</kbd> {t.ask.panelComposerFooter}
|
|
608
|
+
</span>
|
|
609
|
+
) : null}
|
|
610
|
+
<span suppressHydrationWarning>
|
|
611
|
+
<kbd className="font-mono">@</kbd> {t.ask.attachFile}
|
|
612
|
+
</span>
|
|
613
|
+
{!isPanel && (
|
|
614
|
+
<span suppressHydrationWarning>
|
|
615
|
+
<kbd className="font-mono">ESC</kbd> {t.search.close}
|
|
616
|
+
</span>
|
|
617
|
+
)}
|
|
430
618
|
</div>
|
|
431
619
|
</>
|
|
432
620
|
);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, type PointerEvent as ReactPointerEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface UseComposerVerticalResizeOptions {
|
|
6
|
+
minHeight: number;
|
|
7
|
+
maxHeightAbs: number;
|
|
8
|
+
maxHeightViewportRatio: number;
|
|
9
|
+
getHeight: () => number;
|
|
10
|
+
setHeight: (h: number) => void;
|
|
11
|
+
persist: (h: number) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Drag the top edge of a composer upward to grow height (panel input UX).
|
|
16
|
+
* Uses pointer capture so drag ends reliably (window exit, touch, pointercancel).
|
|
17
|
+
*/
|
|
18
|
+
export function useComposerVerticalResize({
|
|
19
|
+
minHeight,
|
|
20
|
+
maxHeightAbs,
|
|
21
|
+
maxHeightViewportRatio,
|
|
22
|
+
getHeight,
|
|
23
|
+
setHeight,
|
|
24
|
+
persist,
|
|
25
|
+
}: UseComposerVerticalResizeOptions) {
|
|
26
|
+
const onPointerDown = useCallback(
|
|
27
|
+
(e: ReactPointerEvent<HTMLElement>) => {
|
|
28
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
const el = e.currentTarget;
|
|
31
|
+
el.setPointerCapture(e.pointerId);
|
|
32
|
+
|
|
33
|
+
const pointerId = e.pointerId;
|
|
34
|
+
const startY = e.clientY;
|
|
35
|
+
const startH = getHeight();
|
|
36
|
+
let currentH = startH;
|
|
37
|
+
|
|
38
|
+
const maxH = () => Math.min(maxHeightAbs, Math.floor(window.innerHeight * maxHeightViewportRatio));
|
|
39
|
+
|
|
40
|
+
document.body.classList.add('select-none');
|
|
41
|
+
document.body.style.cursor = 'ns-resize';
|
|
42
|
+
|
|
43
|
+
const onMove = (ev: PointerEvent) => {
|
|
44
|
+
if (ev.pointerId !== pointerId) return;
|
|
45
|
+
const dy = startY - ev.clientY;
|
|
46
|
+
const next = Math.round(Math.max(minHeight, Math.min(maxH(), startH + dy)));
|
|
47
|
+
currentH = next;
|
|
48
|
+
setHeight(next);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const onUpOrCancel = (ev: PointerEvent) => {
|
|
52
|
+
if (ev.pointerId !== pointerId) return;
|
|
53
|
+
document.body.classList.remove('select-none');
|
|
54
|
+
document.body.style.cursor = '';
|
|
55
|
+
el.removeEventListener('pointermove', onMove);
|
|
56
|
+
el.removeEventListener('pointerup', onUpOrCancel);
|
|
57
|
+
el.removeEventListener('pointercancel', onUpOrCancel);
|
|
58
|
+
try {
|
|
59
|
+
el.releasePointerCapture(pointerId);
|
|
60
|
+
} catch {
|
|
61
|
+
/* already released */
|
|
62
|
+
}
|
|
63
|
+
persist(currentH);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
el.addEventListener('pointermove', onMove);
|
|
67
|
+
el.addEventListener('pointerup', onUpOrCancel);
|
|
68
|
+
el.addEventListener('pointercancel', onUpOrCancel);
|
|
69
|
+
},
|
|
70
|
+
[minHeight, maxHeightAbs, maxHeightViewportRatio, getHeight, setHeight, persist],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return onPointerDown;
|
|
74
|
+
}
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -97,6 +97,11 @@ export const en = {
|
|
|
97
97
|
placeholder: 'Ask a question... or type @ to attach a file',
|
|
98
98
|
emptyPrompt: 'Ask anything about your knowledge base',
|
|
99
99
|
send: 'send',
|
|
100
|
+
newlineHint: 'new line',
|
|
101
|
+
panelComposerResize: 'Drag up to enlarge the input area',
|
|
102
|
+
panelComposerFooter: 'Resize height',
|
|
103
|
+
panelComposerResetHint: 'Double-click to reset height',
|
|
104
|
+
panelComposerKeyboard: 'Arrow keys adjust height; Home/End min/max',
|
|
100
105
|
attachFile: 'attach file',
|
|
101
106
|
attachCurrent: 'attach current file',
|
|
102
107
|
stopTitle: 'Stop',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -122,6 +122,11 @@ export const zh = {
|
|
|
122
122
|
placeholder: '输入问题,或输入 @ 添加附件文件',
|
|
123
123
|
emptyPrompt: '可以问任何关于知识库的问题',
|
|
124
124
|
send: '发送',
|
|
125
|
+
newlineHint: '换行',
|
|
126
|
+
panelComposerResize: '向上拖拽以拉高输入区',
|
|
127
|
+
panelComposerFooter: '拉高输入区',
|
|
128
|
+
panelComposerResetHint: '双击恢复默认高度',
|
|
129
|
+
panelComposerKeyboard: '方向键调节高度;Home/End 最小或最大',
|
|
125
130
|
attachFile: '附加文件',
|
|
126
131
|
attachCurrent: '附加当前文件',
|
|
127
132
|
stopTitle: '停止',
|
package/app/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/app/next.config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
const nextConfig: NextConfig = {
|
|
5
5
|
transpilePackages: ['github-slugger'],
|
|
6
6
|
serverExternalPackages: ['chokidar', 'openai', '@mariozechner/pi-ai', '@mariozechner/pi-agent-core'],
|
|
7
|
+
output: 'standalone',
|
|
7
8
|
outputFileTracingRoot: path.join(__dirname),
|
|
8
9
|
turbopack: {
|
|
9
10
|
root: path.join(__dirname),
|
package/bin/cli.js
CHANGED
|
@@ -394,7 +394,11 @@ const commands = {
|
|
|
394
394
|
startSyncDaemon(mindRoot).catch(() => {});
|
|
395
395
|
}
|
|
396
396
|
await printStartupInfo(webPort, mcpPort);
|
|
397
|
-
run(
|
|
397
|
+
run(
|
|
398
|
+
`${NEXT_BIN} start -p ${webPort} ${extra}`,
|
|
399
|
+
resolve(ROOT, 'app'),
|
|
400
|
+
process.env.HOSTNAME ? undefined : { HOSTNAME: '127.0.0.1' }
|
|
401
|
+
);
|
|
398
402
|
},
|
|
399
403
|
|
|
400
404
|
// ── build ──────────────────────────────────────────────────────────────────
|
package/bin/lib/utils.js
CHANGED
|
@@ -3,9 +3,13 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { ROOT } from './constants.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param {Record<string, string | undefined>} [envPatch] merged into process.env (for child only)
|
|
8
|
+
*/
|
|
9
|
+
export function run(command, cwd = ROOT, envPatch) {
|
|
7
10
|
try {
|
|
8
|
-
|
|
11
|
+
const env = envPatch ? { ...process.env, ...envPatch } : process.env;
|
|
12
|
+
execSync(command, { cwd, stdio: 'inherit', env });
|
|
9
13
|
} catch (err) {
|
|
10
14
|
process.exit(err.status || 1);
|
|
11
15
|
}
|
package/mcp/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geminilight/mindos",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.57",
|
|
4
4
|
"description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mindos",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"start": "mindos start",
|
|
65
65
|
"mcp": "mindos mcp",
|
|
66
66
|
"test": "cd app && npx vitest run",
|
|
67
|
+
"verify:standalone": "node scripts/verify-standalone.mjs",
|
|
67
68
|
"release": "bash scripts/release.sh"
|
|
68
69
|
},
|
|
69
70
|
"engines": {
|
package/scripts/release.sh
CHANGED
|
@@ -28,6 +28,13 @@ else
|
|
|
28
28
|
exit 1
|
|
29
29
|
fi
|
|
30
30
|
cd ..
|
|
31
|
+
echo "🩺 Verifying standalone server (/api/health)..."
|
|
32
|
+
if node scripts/verify-standalone.mjs; then
|
|
33
|
+
echo " ✅ Standalone smoke OK"
|
|
34
|
+
else
|
|
35
|
+
echo "❌ Standalone verify failed (trace / serverExternalPackages?)"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
31
38
|
# Restore any files modified by next build (e.g. next-env.d.ts)
|
|
32
39
|
git checkout -- . 2>/dev/null || true
|
|
33
40
|
echo ""
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Smoke-test Next standalone server: merge static/public, spawn server.js, GET /api/health.
|
|
4
|
+
* Catches missing serverExternalPackages / file-trace gaps (MODULE_NOT_FOUND at startup).
|
|
5
|
+
*
|
|
6
|
+
* Run from repo root after: cd app && ./node_modules/.bin/next build
|
|
7
|
+
* node scripts/verify-standalone.mjs
|
|
8
|
+
*
|
|
9
|
+
* @see wiki/specs/spec-desktop-standalone-runtime.md
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import http from 'http';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { materializeStandaloneAssets } from '../desktop/scripts/prepare-mindos-bundle.mjs';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const root = path.resolve(__dirname, '..');
|
|
20
|
+
const appDir = path.join(root, 'app');
|
|
21
|
+
const serverJs = path.join(appDir, '.next', 'standalone', 'server.js');
|
|
22
|
+
|
|
23
|
+
if (!existsSync(serverJs)) {
|
|
24
|
+
console.error(
|
|
25
|
+
`[verify-standalone] Missing ${serverJs}\nBuild first: cd app && ./node_modules/.bin/next build`
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
materializeStandaloneAssets(appDir);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const port = 31000 + Math.floor(Math.random() * 5000);
|
|
38
|
+
const nodeBin = process.execPath;
|
|
39
|
+
|
|
40
|
+
function waitHealth(timeoutMs) {
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const tick = () => {
|
|
44
|
+
if (Date.now() > deadline) {
|
|
45
|
+
reject(new Error(`Timeout waiting for http://127.0.0.1:${port}/api/health`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const req = http.get(
|
|
49
|
+
`http://127.0.0.1:${port}/api/health`,
|
|
50
|
+
{ timeout: 2000 },
|
|
51
|
+
(res) => {
|
|
52
|
+
let body = '';
|
|
53
|
+
res.on('data', (c) => {
|
|
54
|
+
body += c;
|
|
55
|
+
});
|
|
56
|
+
res.on('end', () => {
|
|
57
|
+
if (res.statusCode === 200) {
|
|
58
|
+
try {
|
|
59
|
+
const j = JSON.parse(body);
|
|
60
|
+
if (j.ok === true && j.service === 'mindos') {
|
|
61
|
+
resolve();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
/* fall through */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
setTimeout(tick, 300);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
req.on('error', () => {
|
|
73
|
+
setTimeout(tick, 300);
|
|
74
|
+
});
|
|
75
|
+
req.on('timeout', () => {
|
|
76
|
+
req.destroy();
|
|
77
|
+
setTimeout(tick, 300);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
tick();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let stderr = '';
|
|
85
|
+
const child = spawn(nodeBin, [serverJs], {
|
|
86
|
+
cwd: appDir,
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
NODE_ENV: 'production',
|
|
90
|
+
PORT: String(port),
|
|
91
|
+
/** Next binds to machine hostname by default; Desktop health checks use 127.0.0.1 */
|
|
92
|
+
HOSTNAME: '127.0.0.1',
|
|
93
|
+
},
|
|
94
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
child.stderr?.on('data', (c) => {
|
|
98
|
+
stderr += c.toString();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function killChild() {
|
|
102
|
+
try {
|
|
103
|
+
child.kill('SIGTERM');
|
|
104
|
+
} catch {
|
|
105
|
+
/* ignore */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
try {
|
|
111
|
+
await waitHealth(90_000);
|
|
112
|
+
console.log(`[verify-standalone] OK (port ${port})`);
|
|
113
|
+
return 0;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
116
|
+
if (stderr.trim()) console.error('--- server stderr (tail) ---\n', stderr.slice(-4000));
|
|
117
|
+
return 1;
|
|
118
|
+
} finally {
|
|
119
|
+
killChild();
|
|
120
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
child.on('error', (err) => {
|
|
125
|
+
console.error('[verify-standalone] spawn failed:', err.message);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
main().then((code) => process.exit(code));
|
package/skills/mindos/SKILL.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
name: mindos
|
|
3
3
|
description: >
|
|
4
4
|
MindOS knowledge base operation guide, only for agent tasks on files inside the MindOS knowledge base.
|
|
5
|
+
Explains core concepts: Space (partitions by how you think), Instruction (agent-wide rules, often in INSTRUCTION.md),
|
|
6
|
+
Skill (how agents read/write/organize the KB via SKILL.md packages). Notes can embody Instructions and Skills.
|
|
5
7
|
Trigger only when the target files are inside the MindOS knowledge base directory.
|
|
6
8
|
Typical requests: "update notes", "search knowledge base", "organize files", "execute SOP",
|
|
7
9
|
"review with our standards", "handoff to another agent", "sync decisions", "append CSV",
|
|
@@ -20,9 +22,19 @@ context automatically when present. User rules override default rules on conflic
|
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
23
|
-
<!-- version: 1.
|
|
25
|
+
<!-- version: 1.2.0 -->
|
|
24
26
|
# MindOS Operating Rules
|
|
25
27
|
|
|
28
|
+
## MindOS concepts
|
|
29
|
+
|
|
30
|
+
Shared vocabulary for the knowledge base and connected agents:
|
|
31
|
+
|
|
32
|
+
- **Space** — Knowledge partitions organized the way you think. You decide the structure, and AI agents follow it to read, write, and manage automatically.
|
|
33
|
+
- **Instruction** — A rules file that all AI agents obey. You write the boundaries once, and every agent connected to your knowledge base follows them.
|
|
34
|
+
- **Skill** — Teaches agents how to operate your knowledge base — reading, writing, organizing. Agents don't guess; they follow the skills you've installed.
|
|
35
|
+
|
|
36
|
+
**Notes as Instruction and Skill** — Instructions and Skills are usually expressed as Markdown in your tree (e.g. root or directory `INSTRUCTION.md`, `SKILL.md` under a skill folder). A note is not only free-form text: it can be the governance layer agents must follow (Instruction) or a procedure package agents load to execute (Skill).
|
|
37
|
+
|
|
26
38
|
## Core Principles
|
|
27
39
|
|
|
28
40
|
- Treat repository state as source of truth.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
name: mindos-zh
|
|
3
3
|
description: >
|
|
4
4
|
MindOS 知识库中文操作指南,仅用于 MindOS 知识库内的 Agent 任务。
|
|
5
|
+
说明核心概念:空间(按思维方式划分的知识分区)、指令(全 Agent 遵守的规则,常见为 INSTRUCTION.md)、
|
|
6
|
+
技能(通过 SKILL.md 等教 Agent 如何读写整理知识库)。笔记可以承载指令与技能。
|
|
5
7
|
仅当操作目标是 MindOS 知识库目录下的文件时触发,典型请求包括"更新笔记""搜索知识库"
|
|
6
8
|
"整理文件""执行 SOP""按团队标准 review""把任务交接给另一个 Agent""同步决策"
|
|
7
9
|
"追加 CSV""复盘这段对话""提炼关键经验""把复盘结果自适应更新到对应文档"
|
|
@@ -18,9 +20,19 @@ description: >
|
|
|
18
20
|
|
|
19
21
|
---
|
|
20
22
|
|
|
21
|
-
<!-- version: 1.
|
|
23
|
+
<!-- version: 1.2.0 -->
|
|
22
24
|
# MindOS 操作规则
|
|
23
25
|
|
|
26
|
+
## MindOS 核心概念
|
|
27
|
+
|
|
28
|
+
与知识库和接入 Agent 共用的术语说明:
|
|
29
|
+
|
|
30
|
+
- **空间(Space)** — 按你的思维方式组织的知识分区。你怎么想,就怎么分,AI Agent 遵循同样的结构来自动读写和管理。
|
|
31
|
+
- **指令(Instruction)** — 一份所有 AI Agent 都遵守的规则文件。你写一次边界,每个连接到知识库的 Agent 都会照做。
|
|
32
|
+
- **技能(Skill)** — 教 Agent 如何操作你的知识库——读取、写入、整理。Agent 不是瞎猜,而是按你安装的 Skill 来执行。
|
|
33
|
+
|
|
34
|
+
**笔记即指令 / 技能** — 指令与技能在知识库里通常体现为 Markdown 文件(例如根或目录下的 `INSTRUCTION.md`、技能目录中的 `SKILL.md`)。笔记不只是随笔:可以是 Agent 必须遵守的治理层(指令),也可以是 Agent 加载后按步骤执行的程序包(技能)。
|
|
35
|
+
|
|
24
36
|
## 核心原则
|
|
25
37
|
|
|
26
38
|
- 以仓库当前状态为唯一依据。
|