@geminilight/mindos 0.5.3 → 0.5.6

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/lib/i18n.ts CHANGED
@@ -171,6 +171,7 @@ export const messages = {
171
171
  enabled: 'Enabled',
172
172
  disabled: 'Disabled',
173
173
  matchHint: 'Auto-activates on',
174
+ coreHint: 'Core renderer — always enabled',
174
175
  noPlugins: 'No renderers installed.',
175
176
  comingSoon: 'Plugin marketplace coming soon.',
176
177
  },
@@ -198,6 +199,7 @@ export const messages = {
198
199
  notInstalled: 'Not installed',
199
200
  transportStdio: 'stdio (recommended)',
200
201
  transportHttp: 'http',
202
+ transportAuto: 'auto (recommended)',
201
203
  httpUrl: 'MCP URL',
202
204
  httpToken: 'Auth Token',
203
205
  installSelected: 'Install Selected',
@@ -221,8 +223,10 @@ export const messages = {
221
223
  skillContent: 'Content',
222
224
  skillNameConflict: 'A skill with this name already exists',
223
225
  skillDeleteConfirm: (name: string) => `Delete skill "${name}"? This cannot be undone.`,
224
- },
225
- save: 'Save',
226
+ skillLanguage: 'Skill Language',
227
+ skillLangEn: 'English',
228
+ skillLangZh: '中文',
229
+ }, save: 'Save',
226
230
  saved: 'Saved',
227
231
  saveFailed: 'Save failed',
228
232
  reconfigure: 'Reconfigure',
@@ -292,7 +296,7 @@ export const messages = {
292
296
  webPort: 'Web UI port',
293
297
  mcpPort: 'MCP server port',
294
298
  portHint: 'Valid range: 1024–65535',
295
- portRestartWarning: 'Port changes take effect after server restart.',
299
+ portRestartWarning: 'The service will start on these ports after setup completes.',
296
300
  portInUse: (p: number) => `Port ${p} is already in use.`,
297
301
  portSuggest: (p: number) => `Use ${p}`,
298
302
  portChecking: 'Checking…',
@@ -324,6 +328,17 @@ export const messages = {
324
328
  agentStatusOk: 'configured',
325
329
  agentStatusError: 'failed',
326
330
  agentInstalling: 'Configuring…',
331
+ agentTransportAuto: 'Auto (recommended)',
332
+ agentTransportLabel: 'Transport',
333
+ agentVerified: 'verified',
334
+ agentUnverified: 'unverified',
335
+ agentVerifyNote: 'stdio agents are verified after restart',
336
+ // Skill auto-install
337
+ skillAutoHint: (name: string) => `Based on your template, the "${name}" skill will be installed to selected agents.`,
338
+ skillLabel: 'Skill',
339
+ skillInstalling: 'Installing skill…',
340
+ skillInstalled: 'Skill installed',
341
+ skillFailed: 'Skill install failed',
327
342
  // Step 2 — AI skip card
328
343
  aiSkipTitle: 'Skip for now',
329
344
  aiSkipDesc: 'You can add an API key later in Settings → AI.',
@@ -525,6 +540,7 @@ export const messages = {
525
540
  enabled: '已启用',
526
541
  disabled: '已禁用',
527
542
  matchHint: '自动匹配',
543
+ coreHint: '核心渲染器 — 始终启用',
528
544
  noPlugins: '暂无渲染器。',
529
545
  comingSoon: '插件市场即将上线。',
530
546
  },
@@ -552,6 +568,7 @@ export const messages = {
552
568
  notInstalled: '未安装',
553
569
  transportStdio: 'stdio(推荐)',
554
570
  transportHttp: 'http',
571
+ transportAuto: '自动(推荐)',
555
572
  httpUrl: 'MCP URL',
556
573
  httpToken: '认证 Token',
557
574
  installSelected: '安装选中',
@@ -575,8 +592,10 @@ export const messages = {
575
592
  skillContent: '内容',
576
593
  skillNameConflict: '同名 skill 已存在',
577
594
  skillDeleteConfirm: (name: string) => `确定删除「${name}」?此操作不可撤销。`,
578
- },
579
- save: '保存',
595
+ skillLanguage: 'Skill 语言',
596
+ skillLangEn: 'English',
597
+ skillLangZh: '中文',
598
+ }, save: '保存',
580
599
  saved: '已保存',
581
600
  saveFailed: '保存失败',
582
601
  reconfigure: '重新配置',
@@ -646,7 +665,7 @@ export const messages = {
646
665
  webPort: 'Web UI 端口',
647
666
  mcpPort: 'MCP 服务端口',
648
667
  portHint: '有效范围:1024–65535',
649
- portRestartWarning: '端口修改需重启服务后生效。',
668
+ portRestartWarning: '完成配置后,服务将以这些端口启动。',
650
669
  portInUse: (p: number) => `端口 ${p} 已被占用。`,
651
670
  portSuggest: (p: number) => `使用 ${p}`,
652
671
  portChecking: '检测中…',
@@ -678,6 +697,17 @@ export const messages = {
678
697
  agentStatusOk: '已配置',
679
698
  agentStatusError: '失败',
680
699
  agentInstalling: '配置中…',
700
+ agentTransportAuto: '自动(推荐)',
701
+ agentTransportLabel: '传输方式',
702
+ agentVerified: '已验证',
703
+ agentUnverified: '未验证',
704
+ agentVerifyNote: 'stdio agent 需重启后验证',
705
+ // Skill auto-install
706
+ skillAutoHint: (name: string) => `根据您选择的模板,将向选中的 Agent 安装「${name}」Skill。`,
707
+ skillLabel: 'Skill',
708
+ skillInstalling: '正在安装 Skill…',
709
+ skillInstalled: 'Skill 已安装',
710
+ skillFailed: 'Skill 安装失败',
681
711
  // Step 2 — AI skip card
682
712
  aiSkipTitle: '暂时跳过',
683
713
  aiSkipDesc: '稍后可在 设置 → AI 中添加 API 密钥。',
@@ -11,18 +11,19 @@ export interface AgentDef {
11
11
  project: string | null;
12
12
  global: string;
13
13
  key: string;
14
+ preferredTransport: 'stdio' | 'http';
14
15
  }
15
16
 
16
17
  export const MCP_AGENTS: Record<string, AgentDef> = {
17
- 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
18
- 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
19
- 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
20
- 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
21
- 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
22
- 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
23
- 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
24
- 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
25
- 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
18
+ 'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
19
+ 'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers', preferredTransport: 'http' },
20
+ 'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
21
+ 'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
22
+ 'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
23
+ 'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
24
+ 'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
25
+ 'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
26
+ 'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
26
27
  };
27
28
 
28
29
  export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
@@ -8,14 +8,14 @@ import { manifest as backlinks } from '@/components/renderers/backlinks/manifest
8
8
  import { manifest as config } from '@/components/renderers/config/manifest';
9
9
  import { manifest as csv } from '@/components/renderers/csv/manifest';
10
10
  import { manifest as diff } from '@/components/renderers/diff/manifest';
11
- import { manifest as graph } from '@/components/renderers/graph/manifest';
12
11
  import { manifest as summary } from '@/components/renderers/summary/manifest';
13
12
  import { manifest as timeline } from '@/components/renderers/timeline/manifest';
14
13
  import { manifest as todo } from '@/components/renderers/todo/manifest';
15
14
  import { manifest as workflow } from '@/components/renderers/workflow/manifest';
15
+ import { manifest as graph } from '@/components/renderers/graph/manifest';
16
16
 
17
17
  const manifests = [
18
- agentInspector, backlinks, config, csv, diff, graph, summary, timeline, todo, workflow,
18
+ agentInspector, backlinks, config, csv, diff, summary, timeline, todo, workflow, graph,
19
19
  ];
20
20
 
21
21
  for (const m of manifests) {
@@ -15,6 +15,7 @@ export interface RendererDefinition {
15
15
  icon: string; // emoji or short string
16
16
  tags: string[];
17
17
  builtin: boolean; // true = ships with MindOS; false = user-installed (future)
18
+ core?: boolean; // true = default renderer for a file type, cannot be disabled by user
18
19
  entryPath?: string; // canonical entry file shown on home page (e.g. 'TODO.md')
19
20
  match: (ctx: Pick<RendererContext, 'filePath' | 'extension'>) => boolean;
20
21
  // Provide either `component` (eager) or `load` (lazy). Prefer `load` for code-splitting.
@@ -38,6 +39,9 @@ export function loadDisabledState() {
38
39
  }
39
40
 
40
41
  export function setRendererEnabled(id: string, enabled: boolean) {
42
+ // Core renderers cannot be disabled
43
+ const def = registry.find(r => r.id === id);
44
+ if (def?.core) return;
41
45
  if (enabled) {
42
46
  _disabledIds.delete(id);
43
47
  } else {
@@ -49,6 +53,9 @@ export function setRendererEnabled(id: string, enabled: boolean) {
49
53
  }
50
54
 
51
55
  export function isRendererEnabled(id: string): boolean {
56
+ // Core renderers cannot be disabled
57
+ const def = registry.find(r => r.id === id);
58
+ if (def?.core) return true;
52
59
  return !_disabledIds.has(id);
53
60
  }
54
61
 
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore, useCallback, useRef } from 'react';
4
+
5
+ /**
6
+ * Unified per-file state hook for renderers.
7
+ *
8
+ * Each renderer stores its state under a namespaced localStorage key:
9
+ * `mindos-renderer:{rendererId}:{filePath}`
10
+ *
11
+ * Usage:
12
+ * const [cfg, setCfg] = useRendererState<CsvConfig>('csv', filePath, defaultCfg);
13
+ *
14
+ * The state is reactive — changes from other tabs/windows or from other
15
+ * components calling the setter will trigger a re-render via
16
+ * `useSyncExternalStore`.
17
+ */
18
+
19
+ const CHANGE_EVENT = 'mindos-renderer-state-change';
20
+
21
+ function storageKey(rendererId: string, filePath: string): string {
22
+ return `mindos-renderer:${rendererId}:${filePath}`;
23
+ }
24
+
25
+ export function useRendererState<T>(
26
+ rendererId: string,
27
+ filePath: string,
28
+ defaultValue: T,
29
+ ): [T, (value: T | ((prev: T) => T)) => void] {
30
+ const key = storageKey(rendererId, filePath);
31
+
32
+ // Cache parsed value to maintain referential stability for useSyncExternalStore.
33
+ // Without this, JSON.parse returns a new object on every getSnapshot call,
34
+ // causing Object.is to fail → infinite re-renders for non-primitive types.
35
+ const cacheRef = useRef<{ key: string; raw: string | null; parsed: T }>({ key, raw: null, parsed: defaultValue });
36
+
37
+ // Reset cache when key changes (different file or different renderer)
38
+ if (cacheRef.current.key !== key) {
39
+ cacheRef.current = { key, raw: null, parsed: defaultValue };
40
+ }
41
+
42
+ const state = useSyncExternalStore(
43
+ (onStoreChange) => {
44
+ const listener = () => onStoreChange();
45
+ window.addEventListener('storage', listener);
46
+ window.addEventListener(CHANGE_EVENT, listener);
47
+ return () => {
48
+ window.removeEventListener('storage', listener);
49
+ window.removeEventListener(CHANGE_EVENT, listener);
50
+ };
51
+ },
52
+ () => {
53
+ try {
54
+ const raw = localStorage.getItem(key);
55
+ if (raw === cacheRef.current.raw) return cacheRef.current.parsed;
56
+ if (raw === null) {
57
+ cacheRef.current = { key, raw: null, parsed: defaultValue };
58
+ return defaultValue;
59
+ }
60
+ const parsed = JSON.parse(raw) as T;
61
+ cacheRef.current = { key, raw, parsed };
62
+ return parsed;
63
+ } catch {
64
+ return defaultValue;
65
+ }
66
+ },
67
+ () => defaultValue,
68
+ );
69
+
70
+ const setState = useCallback(
71
+ (value: T | ((prev: T) => T)) => {
72
+ try {
73
+ const current = (() => {
74
+ try {
75
+ const raw = localStorage.getItem(key);
76
+ return raw !== null ? (JSON.parse(raw) as T) : defaultValue;
77
+ } catch {
78
+ return defaultValue;
79
+ }
80
+ })();
81
+ const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;
82
+ const serialized = JSON.stringify(next);
83
+ localStorage.setItem(key, serialized);
84
+ // Update cache eagerly so the next getSnapshot returns stable ref
85
+ cacheRef.current = { key, raw: serialized, parsed: next };
86
+ } catch { /* ignore quota errors */ }
87
+ window.dispatchEvent(new Event(CHANGE_EVENT));
88
+ },
89
+ [key, defaultValue],
90
+ );
91
+
92
+ return [state, setState];
93
+ }
94
+
95
+ /**
96
+ * Non-hook helpers for reading/writing renderer state outside React components.
97
+ */
98
+ export function getRendererState<T>(rendererId: string, filePath: string, defaultValue: T): T {
99
+ if (typeof window === 'undefined') return defaultValue;
100
+ try {
101
+ const raw = localStorage.getItem(storageKey(rendererId, filePath));
102
+ return raw !== null ? (JSON.parse(raw) as T) : defaultValue;
103
+ } catch {
104
+ return defaultValue;
105
+ }
106
+ }
107
+
108
+ export function setRendererState<T>(rendererId: string, filePath: string, value: T): void {
109
+ if (typeof window === 'undefined') return;
110
+ try {
111
+ localStorage.setItem(storageKey(rendererId, filePath), JSON.stringify(value));
112
+ } catch { /* ignore */ }
113
+ window.dispatchEvent(new Event(CHANGE_EVENT));
114
+ }