@geminilight/mindos 0.5.55 → 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/components/echo/EchoSegmentPageClient.tsx +27 -11
- package/app/hooks/useComposerVerticalResize.ts +74 -0
- package/app/lib/i18n-en.ts +27 -8
- package/app/lib/i18n-zh.ts +29 -8
- 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
|
);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useCallback, useEffect, useId, useMemo, useState } from 'react';
|
|
4
4
|
import type { EchoSegment } from '@/lib/echo-segments';
|
|
5
5
|
import { buildEchoInsightUserPrompt } from '@/lib/echo-insight-prompt';
|
|
6
|
-
import type { Locale } from '@/lib/i18n';
|
|
6
|
+
import type { Locale, Messages } from '@/lib/i18n';
|
|
7
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
8
|
import { openAskModal } from '@/hooks/useAskModal';
|
|
9
9
|
import { EchoHero } from './EchoHero';
|
|
@@ -51,6 +51,21 @@ const inputClass =
|
|
|
51
51
|
const cardSectionClass =
|
|
52
52
|
'rounded-xl border border-border bg-card p-5 shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/20 hover:shadow-md sm:p-6';
|
|
53
53
|
|
|
54
|
+
function echoSnapshotCopy(segment: EchoSegment, p: Messages['echoPages']): { title: string; body: string } {
|
|
55
|
+
switch (segment) {
|
|
56
|
+
case 'about-you':
|
|
57
|
+
return { title: p.snapshotAboutYouTitle, body: p.snapshotAboutYouBody };
|
|
58
|
+
case 'continued':
|
|
59
|
+
return { title: p.snapshotContinuedTitle, body: p.snapshotContinuedBody };
|
|
60
|
+
case 'daily':
|
|
61
|
+
return { title: p.snapshotDailyTitle, body: p.snapshotDailyBody };
|
|
62
|
+
case 'past-you':
|
|
63
|
+
return { title: p.snapshotPastYouTitle, body: p.snapshotPastYouBody };
|
|
64
|
+
case 'growth':
|
|
65
|
+
return { title: p.snapshotGrowthTitle, body: p.snapshotGrowthBody };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
export default function EchoSegmentPageClient({ segment }: { segment: EchoSegment }) {
|
|
55
70
|
const { t, locale } = useLocale();
|
|
56
71
|
const p = t.echoPages;
|
|
@@ -63,6 +78,8 @@ export default function EchoSegmentPageClient({ segment }: { segment: EchoSegmen
|
|
|
63
78
|
const [dailyLine, setDailyLine] = useState('');
|
|
64
79
|
const [growthIntent, setGrowthIntent] = useState('');
|
|
65
80
|
|
|
81
|
+
const snapshot = useMemo(() => echoSnapshotCopy(segment, p), [segment, p]);
|
|
82
|
+
|
|
66
83
|
useEffect(() => {
|
|
67
84
|
try {
|
|
68
85
|
const d = localStorage.getItem(STORAGE_DAILY);
|
|
@@ -106,8 +123,8 @@ export default function EchoSegmentPageClient({ segment }: { segment: EchoSegmen
|
|
|
106
123
|
segment,
|
|
107
124
|
segmentTitle: title,
|
|
108
125
|
factsHeading: p.factsHeading,
|
|
109
|
-
emptyTitle:
|
|
110
|
-
emptyBody:
|
|
126
|
+
emptyTitle: snapshot.title,
|
|
127
|
+
emptyBody: snapshot.body,
|
|
111
128
|
continuedDrafts: p.continuedDrafts,
|
|
112
129
|
continuedTodos: p.continuedTodos,
|
|
113
130
|
subEmptyHint: p.subEmptyHint,
|
|
@@ -121,8 +138,7 @@ export default function EchoSegmentPageClient({ segment }: { segment: EchoSegmen
|
|
|
121
138
|
segment,
|
|
122
139
|
title,
|
|
123
140
|
p.factsHeading,
|
|
124
|
-
|
|
125
|
-
p.emptyFactsBody,
|
|
141
|
+
snapshot,
|
|
126
142
|
p.continuedDrafts,
|
|
127
143
|
p.continuedTodos,
|
|
128
144
|
p.subEmptyHint,
|
|
@@ -133,8 +149,6 @@ export default function EchoSegmentPageClient({ segment }: { segment: EchoSegmen
|
|
|
133
149
|
],
|
|
134
150
|
);
|
|
135
151
|
|
|
136
|
-
const primaryBtnClass =
|
|
137
|
-
'inline-flex items-center rounded-lg bg-primary px-4 py-2.5 font-sans text-sm font-medium text-primary-foreground transition-opacity duration-150 hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
138
152
|
const secondaryBtnClass =
|
|
139
153
|
'inline-flex items-center rounded-lg border border-border bg-background px-4 py-2.5 font-sans text-sm font-medium text-foreground transition-colors duration-150 hover:border-[var(--amber)]/35 hover:bg-[var(--amber-dim)]/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
140
154
|
|
|
@@ -160,8 +174,8 @@ export default function EchoSegmentPageClient({ segment }: { segment: EchoSegmen
|
|
|
160
174
|
headingId={factsHeadingId}
|
|
161
175
|
heading={p.factsHeading}
|
|
162
176
|
snapshotBadge={p.snapshotBadge}
|
|
163
|
-
emptyTitle={
|
|
164
|
-
emptyBody={
|
|
177
|
+
emptyTitle={snapshot.title}
|
|
178
|
+
emptyBody={snapshot.body}
|
|
165
179
|
actions={
|
|
166
180
|
segment === 'about-you' ? (
|
|
167
181
|
<button type="button" onClick={openSegmentAsk} className={secondaryBtnClass}>
|
|
@@ -198,8 +212,9 @@ export default function EchoSegmentPageClient({ segment }: { segment: EchoSegmen
|
|
|
198
212
|
placeholder={p.dailyLinePlaceholder}
|
|
199
213
|
className={inputClass}
|
|
200
214
|
/>
|
|
215
|
+
<p className="mt-3 font-sans text-2xs text-muted-foreground">{p.dailySavedNote}</p>
|
|
201
216
|
<div className="mt-4">
|
|
202
|
-
<button type="button" onClick={openDailyAsk} className={
|
|
217
|
+
<button type="button" onClick={openDailyAsk} className={secondaryBtnClass}>
|
|
203
218
|
{p.continueAgent}
|
|
204
219
|
</button>
|
|
205
220
|
</div>
|
|
@@ -231,11 +246,12 @@ export default function EchoSegmentPageClient({ segment }: { segment: EchoSegmen
|
|
|
231
246
|
|
|
232
247
|
{segment === 'past-you' ? (
|
|
233
248
|
<section className={`${cardSectionClass} mt-6`}>
|
|
249
|
+
<label className={fieldLabelClass}>{p.pastYouDrawLabel}</label>
|
|
234
250
|
<button
|
|
235
251
|
type="button"
|
|
236
252
|
disabled
|
|
237
253
|
title={p.pastYouDisabledHint}
|
|
238
|
-
className="inline-flex cursor-not-allowed items-center rounded-lg border border-dashed border-border bg-muted/20 px-4 py-2.5 font-sans text-sm text-muted-foreground opacity-85"
|
|
254
|
+
className="mt-2 inline-flex cursor-not-allowed items-center rounded-lg border border-dashed border-border bg-muted/20 px-4 py-2.5 font-sans text-sm text-muted-foreground opacity-85"
|
|
239
255
|
>
|
|
240
256
|
{p.pastYouAnother}
|
|
241
257
|
</button>
|
|
@@ -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',
|
|
@@ -204,14 +209,26 @@ export const en = {
|
|
|
204
209
|
segmentNavAria: 'Echo sections',
|
|
205
210
|
snapshotBadge: 'Local · private',
|
|
206
211
|
factsHeading: 'Snapshot',
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
'
|
|
212
|
+
snapshotAboutYouTitle: 'Clues will gather here',
|
|
213
|
+
snapshotAboutYouBody:
|
|
214
|
+
'Soon this will list notes whose paths, links, or titles curve back toward you—each one opens in the editor. Aggregation is still wiring up; for now, use the button below to talk it through in MindOS Agent.',
|
|
215
|
+
snapshotContinuedTitle: 'Drafts and open loops',
|
|
216
|
+
snapshotContinuedBody:
|
|
217
|
+
'This will hold untitled drafts, half-finished pieces, and unchecked tasks. The list feed is on the way; the two columns set the rhythm early.',
|
|
218
|
+
snapshotDailyTitle: 'Start with one line',
|
|
219
|
+
snapshotDailyBody:
|
|
220
|
+
'Your line for today saves in this browser only, as soon as you leave the field. Echo does not have to be an essay—one line is enough; open Agent when you want depth.',
|
|
221
|
+
snapshotPastYouTitle: 'A glance at the past',
|
|
222
|
+
snapshotPastYouBody:
|
|
223
|
+
'We will gently sample an older note so you can see who you were at another point—not a diff tool, just a glimpse. Sampling arrives in a later pass.',
|
|
224
|
+
snapshotGrowthTitle: 'Intent lives here',
|
|
225
|
+
snapshotGrowthBody:
|
|
226
|
+
'Below, write one sentence about what you are steering toward. It stays on this device and can change over time; later you may see how it drifts.',
|
|
210
227
|
insightTitle: 'Insight',
|
|
211
228
|
insightShow: 'Show insight',
|
|
212
229
|
insightHide: 'Hide insight',
|
|
213
230
|
insightHint:
|
|
214
|
-
'
|
|
231
|
+
'Uses only what is visible on this page—including this snapshot and anything you typed here—not your full library. Configure AI under Settings first.',
|
|
215
232
|
generateInsight: 'Generate insight',
|
|
216
233
|
generateInsightNoAi: 'Add an API key under Settings → AI (or set env vars), then refresh this page.',
|
|
217
234
|
insightGenerating: 'Generating…',
|
|
@@ -220,18 +237,20 @@ export const en = {
|
|
|
220
237
|
continueAgent: 'Continue in MindOS Agent',
|
|
221
238
|
continuedDrafts: 'Drafts',
|
|
222
239
|
continuedTodos: 'Open loops',
|
|
223
|
-
subEmptyHint: '
|
|
240
|
+
subEmptyHint: 'Items will appear in each column once the library feed is connected.',
|
|
224
241
|
dailyLineLabel: 'A line for today',
|
|
225
242
|
dailyLinePlaceholder: 'Write one quiet line…',
|
|
243
|
+
dailySavedNote: 'Saved in this browser; visible only on this device.',
|
|
226
244
|
dailyAskPrefill: (line: string) =>
|
|
227
245
|
`Echo / Daily — reflect on this line:\n\n${line.trim() || '(empty line)'}`,
|
|
246
|
+
pastYouDrawLabel: 'A draw from the past',
|
|
228
247
|
pastYouAnother: 'Draw another moment',
|
|
229
|
-
pastYouDisabledHint: '
|
|
248
|
+
pastYouDisabledHint: 'We are connecting time and your library; soon a tap here will surface an old excerpt.',
|
|
230
249
|
growthIntentLabel: 'What you are steering toward',
|
|
231
250
|
growthIntentPlaceholder: 'Write your current intent…',
|
|
232
251
|
growthSavedNote: 'Saved on this device.',
|
|
233
|
-
aboutYouLead: '
|
|
234
|
-
continuedLead: 'Pick up
|
|
252
|
+
aboutYouLead: 'Sense what curves toward you—without digging through every folder.',
|
|
253
|
+
continuedLead: 'Pick up the sentence you left mid-air.',
|
|
235
254
|
dailyLead: 'One quiet line for today — no need to open a full chat.',
|
|
236
255
|
pastYouLead: 'Choices and moods you set down at another point on the timeline.',
|
|
237
256
|
growthLead: 'What you are steering toward, and how it slowly shifts.',
|
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: '停止',
|
|
@@ -228,12 +233,26 @@ export const zh = {
|
|
|
228
233
|
segmentNavAria: '回响模块',
|
|
229
234
|
snapshotBadge: '本地 · 私密',
|
|
230
235
|
factsHeading: '所见',
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
snapshotAboutYouTitle: '线索会住在这里',
|
|
237
|
+
snapshotAboutYouBody:
|
|
238
|
+
'以后这里会列出路径、链接或标题里「绕回你」的笔记,点开即读。文库聚合尚在接入;若想现在就着「与我相关」整理或深聊,可用下方按钮打开 MindOS Agent。',
|
|
239
|
+
snapshotContinuedTitle: '草稿与未收口',
|
|
240
|
+
snapshotContinuedBody:
|
|
241
|
+
'这里将汇集未命名草稿、写到一半的稿,以及待办里尚未勾上的项。列表能力正在接入;两栏结构先帮你建立心理预期。',
|
|
242
|
+
snapshotDailyTitle: '今天,从一小行开始',
|
|
243
|
+
snapshotDailyBody:
|
|
244
|
+
'下方「今日一行」写在浏览器本机,离手即存。回响不必写成文章——一行就够,需要展开时再找 Agent。',
|
|
245
|
+
snapshotPastYouTitle: '随机一瞥往日',
|
|
246
|
+
snapshotPastYouBody:
|
|
247
|
+
'我们会从时间轴里温和地抽一篇旧笔记的片段,让你看见那个时间点的自己:不是对比工具,只是一眼。抽样能力随后就到。',
|
|
248
|
+
snapshotGrowthTitle: '意图落在这里',
|
|
249
|
+
snapshotGrowthBody:
|
|
250
|
+
'下方可写一句「此刻最想推进的方向」。它只保存在本机,可随时间改写;日后可在此看见缓慢的变化。',
|
|
233
251
|
insightTitle: '见解',
|
|
234
252
|
insightShow: '展开见解',
|
|
235
253
|
insightHide: '收起见解',
|
|
236
|
-
insightHint:
|
|
254
|
+
insightHint:
|
|
255
|
+
'只会带上本页可见的文字(含「所见」与你在本页输入的内容),不会默认上传整库。需在 设置 → AI 中配置后再用。',
|
|
237
256
|
generateInsight: '生成见解',
|
|
238
257
|
generateInsightNoAi: '请在 设置 → AI 中填写 API Key(或配置环境变量)后刷新本页。',
|
|
239
258
|
insightGenerating: '生成中…',
|
|
@@ -242,20 +261,22 @@ export const zh = {
|
|
|
242
261
|
continueAgent: '在 MindOS Agent 中继续',
|
|
243
262
|
continuedDrafts: '草稿',
|
|
244
263
|
continuedTodos: '未收口',
|
|
245
|
-
subEmptyHint: '
|
|
264
|
+
subEmptyHint: '接入文库后,条目会出现在对应分组里。',
|
|
246
265
|
dailyLineLabel: '今日一行',
|
|
247
266
|
dailyLinePlaceholder: '写下一行轻量的文字…',
|
|
267
|
+
dailySavedNote: '已保存在本机浏览器;仅本设备可见。',
|
|
248
268
|
dailyAskPrefill: (line: string) =>
|
|
249
269
|
`回响 / 每日 — 围绕这一行帮我展开:\n\n${line.trim() || '(空行)'}`,
|
|
270
|
+
pastYouDrawLabel: '往日一瞥',
|
|
250
271
|
pastYouAnother: '再抽一笔',
|
|
251
|
-
pastYouDisabledHint: '
|
|
272
|
+
pastYouDisabledHint: '正在连接时间与文库,稍后在此轻点即可抽读旧笔记片段。',
|
|
252
273
|
growthIntentLabel: '当前意图',
|
|
253
274
|
growthIntentPlaceholder: '写下你正在推进的方向…',
|
|
254
275
|
growthSavedNote: '已保存在本机。',
|
|
255
|
-
aboutYouLead: '
|
|
256
|
-
continuedLead: '
|
|
276
|
+
aboutYouLead: '不经翻找,也能感到有什么在轻轻指向你。',
|
|
277
|
+
continuedLead: '上次停下的句号,在这里接着写。',
|
|
257
278
|
dailyLead: '给今天留一行空白;轻到不必为此开一场对话。',
|
|
258
|
-
pastYouLead: '
|
|
279
|
+
pastYouLead: '在另一个时间刻度上,瞥见你写下的选择与心情。',
|
|
259
280
|
growthLead: '你在推的方向,以及它怎样慢慢偏转。',
|
|
260
281
|
},
|
|
261
282
|
shortcutPanel: {
|
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
|
- 以仓库当前状态为唯一依据。
|