@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.
@@ -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 inputRef = useRef<HTMLInputElement>(null);
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((e: React.KeyboardEvent<HTMLInputElement>) => {
112
- if (mention.mentionQuery === null) return;
113
- if (e.key === 'ArrowDown') {
114
- e.preventDefault();
115
- mention.navigateMention('down');
116
- } else if (e.key === 'ArrowUp') {
117
- e.preventDefault();
118
- mention.navigateMention('up');
119
- } else if (e.key === 'Enter' || e.key === 'Tab') {
120
- if (mention.mentionResults.length > 0) {
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
- selectMention(mention.mentionResults[mention.mentionIndex]);
234
+ (e.currentTarget as HTMLTextAreaElement).form?.requestSubmit();
123
235
  }
124
- }
125
- }, [mention, selectMention]);
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 className="border-t border-border shrink-0">
324
- {attachedFiles.length > 0 && (
325
- <div className={isPanel ? 'px-3 pt-2 pb-1' : 'px-4 pt-2.5 pb-1'}>
326
- <div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
327
- {isPanel ? 'Context' : 'Knowledge Base Context'}
328
- </div>
329
- <div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
330
- {attachedFiles.map(f => (
331
- <FileChip key={f} path={f} onRemove={() => setAttachedFiles(prev => prev.filter(x => x !== f))} />
332
- ))}
333
- </div>
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
- {upload.localAttachments.length > 0 && (
338
- <div className={isPanel ? 'px-3 pb-1' : 'px-4 pb-1'}>
339
- <div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
340
- {isPanel ? 'Uploaded' : 'Uploaded Files'}
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
- <div className={`flex flex-wrap ${isPanel ? 'gap-1' : 'gap-1.5'}`}>
343
- {upload.localAttachments.map((f, idx) => (
344
- <FileChip key={`${f.name}-${idx}`} path={f.name} variant="upload" onRemove={() => upload.removeAttachment(idx)} />
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
- </div>
348
- )}
486
+ )}
349
487
 
350
- {upload.uploadError && (
351
- <div className={`${isPanel ? 'px-3' : 'px-4'} pb-1 text-xs text-error`}>{upload.uploadError}</div>
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
- {mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
355
- <MentionPopover
356
- results={mention.mentionResults}
357
- selectedIndex={mention.mentionIndex}
358
- onSelect={selectMention}
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
- <form onSubmit={handleSubmit} className={`flex items-center ${isPanel ? 'gap-1.5 px-2 py-2.5' : 'gap-2 px-3 py-3'}`}>
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(() => { el.focus(); el.setSelectionRange(pos + 1, pos + 1); }, 0);
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
- <input
397
- ref={inputRef}
398
- value={input}
399
- onChange={e => handleInputChange(e.target.value)}
400
- onKeyDown={handleInputKeyDown}
401
- placeholder={t.ask.placeholder}
402
- disabled={isLoading}
403
- className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50 min-w-0"
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
- </form>
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><kbd className="font-mono">↵</kbd> {t.ask.send}</span>
428
- <span><kbd className="font-mono">@</kbd> {t.ask.attachFile}</span>
429
- {!isPanel && <span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>}
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: p.emptyFactsTitle,
110
- emptyBody: p.emptyFactsBody,
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
- p.emptyFactsTitle,
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={p.emptyFactsTitle}
164
- emptyBody={p.emptyFactsBody}
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={primaryBtnClass}>
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
+ }
@@ -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
- emptyFactsTitle: 'Nothing here yet',
208
- emptyFactsBody:
209
- 'Structured clues from your library will appear here when indexing is enabled. Data stays on your device.',
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
- 'When enabled, only the items listed above are sent as context. There is no full-library upload by default.',
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: 'No items in this group yet.',
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: 'Random sampling arrives in a later update.',
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: 'Paths and links in your library that curve back to your name.',
234
- continuedLead: 'Pick up where you left off.',
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.',
@@ -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
- emptyFactsTitle: '尚无内容',
232
- emptyFactsBody: '开启索引后,笔记库中的结构化线索会出现在此。数据仅保存在本机。',
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: {
@@ -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(`${NEXT_BIN} start -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
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
- export function run(command, cwd = ROOT) {
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
- execSync(command, { cwd, stdio: 'inherit', env: process.env });
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
@@ -8,11 +8,11 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@modelcontextprotocol/sdk": "^1.25.0",
11
+ "tsx": "^4.19.0",
11
12
  "zod": "^3.23.8"
12
13
  },
13
14
  "devDependencies": {
14
15
  "@types/node": "^22",
15
- "tsx": "^4.19.0",
16
16
  "typescript": "^5"
17
17
  }
18
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.55",
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": {
@@ -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));
@@ -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.1.0 -->
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.1.0 -->
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
  - 以仓库当前状态为唯一依据。