@geminilight/mindos 0.3.0 → 0.4.0

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.
Files changed (76) hide show
  1. package/app/app/api/mcp/agents/route.ts +72 -0
  2. package/app/app/api/mcp/install/route.ts +95 -0
  3. package/app/app/api/mcp/status/route.ts +47 -0
  4. package/app/app/api/skills/route.ts +208 -0
  5. package/app/app/api/sync/route.ts +54 -3
  6. package/app/app/api/update-check/route.ts +52 -0
  7. package/app/app/globals.css +12 -0
  8. package/app/app/layout.tsx +4 -2
  9. package/app/app/login/page.tsx +20 -13
  10. package/app/app/page.tsx +17 -2
  11. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  12. package/app/app/view/[...path]/loading.tsx +1 -1
  13. package/app/app/view/[...path]/not-found.tsx +101 -0
  14. package/app/components/AskFab.tsx +1 -1
  15. package/app/components/AskModal.tsx +1 -1
  16. package/app/components/Backlinks.tsx +1 -1
  17. package/app/components/Breadcrumb.tsx +13 -3
  18. package/app/components/CsvView.tsx +5 -6
  19. package/app/components/DirView.tsx +42 -21
  20. package/app/components/FindInPage.tsx +211 -0
  21. package/app/components/HomeContent.tsx +97 -44
  22. package/app/components/JsonView.tsx +1 -2
  23. package/app/components/MarkdownEditor.tsx +1 -2
  24. package/app/components/OnboardingView.tsx +6 -7
  25. package/app/components/SettingsModal.tsx +5 -2
  26. package/app/components/SetupWizard.tsx +4 -4
  27. package/app/components/Sidebar.tsx +1 -1
  28. package/app/components/UpdateBanner.tsx +101 -0
  29. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  30. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  31. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  32. package/app/components/renderers/backlinks/manifest.ts +14 -0
  33. package/app/components/renderers/config/manifest.ts +14 -0
  34. package/app/components/renderers/csv/BoardView.tsx +12 -12
  35. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  36. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  37. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  38. package/app/components/renderers/csv/TableView.tsx +4 -5
  39. package/app/components/renderers/csv/manifest.ts +14 -0
  40. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  41. package/app/components/renderers/diff/manifest.ts +14 -0
  42. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  43. package/app/components/renderers/graph/manifest.ts +14 -0
  44. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  45. package/app/components/renderers/summary/manifest.ts +14 -0
  46. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  47. package/app/components/renderers/timeline/manifest.ts +14 -0
  48. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  49. package/app/components/renderers/todo/manifest.ts +14 -0
  50. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  51. package/app/components/renderers/workflow/manifest.ts +14 -0
  52. package/app/components/settings/McpTab.tsx +549 -0
  53. package/app/components/settings/SyncTab.tsx +139 -50
  54. package/app/components/settings/types.ts +1 -1
  55. package/app/data/pages/home.png +0 -0
  56. package/app/lib/i18n.ts +178 -10
  57. package/app/lib/renderers/index.ts +20 -89
  58. package/app/lib/renderers/registry.ts +4 -1
  59. package/app/lib/settings.ts +3 -0
  60. package/app/package.json +1 -0
  61. package/app/types/semver.d.ts +8 -0
  62. package/bin/cli.js +137 -24
  63. package/bin/lib/build.js +53 -18
  64. package/bin/lib/colors.js +3 -1
  65. package/bin/lib/config.js +4 -0
  66. package/bin/lib/constants.js +2 -0
  67. package/bin/lib/debug.js +10 -0
  68. package/bin/lib/startup.js +21 -20
  69. package/bin/lib/stop.js +41 -3
  70. package/bin/lib/sync.js +65 -53
  71. package/bin/lib/update-check.js +94 -0
  72. package/bin/lib/utils.js +2 -2
  73. package/package.json +1 -1
  74. package/scripts/gen-renderer-index.js +57 -0
  75. package/scripts/setup.js +24 -0
  76. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { X } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { apiFetch } from '@/lib/api';
7
+
8
+ interface UpdateInfo {
9
+ current: string;
10
+ latest: string;
11
+ }
12
+
13
+ export default function UpdateBanner() {
14
+ const { t } = useLocale();
15
+ const [info, setInfo] = useState<UpdateInfo | null>(null);
16
+
17
+ useEffect(() => {
18
+ // Don't check for updates on setup or login pages
19
+ if (typeof window !== 'undefined') {
20
+ const path = window.location.pathname;
21
+ if (path === '/setup' || path === '/login') return;
22
+ }
23
+
24
+ const timer = setTimeout(async () => {
25
+ try {
26
+ const data = await apiFetch<{ hasUpdate: boolean; latest: string; current: string }>('/api/update-check');
27
+ if (!data.hasUpdate) return;
28
+
29
+ const dismissed = localStorage.getItem('mindos_update_dismissed');
30
+ if (data.latest === dismissed) return;
31
+
32
+ setInfo({ latest: data.latest, current: data.current });
33
+ } catch {
34
+ // Network error / API failure — silent
35
+ }
36
+ }, 3000); // Check 3s after page load, don't block first paint
37
+
38
+ return () => clearTimeout(timer);
39
+ }, []);
40
+
41
+ if (!info) return null;
42
+
43
+ const handleDismiss = () => {
44
+ localStorage.setItem('mindos_update_dismissed', info.latest);
45
+ setInfo(null);
46
+ };
47
+
48
+ const updateT = t.updateBanner;
49
+
50
+ return (
51
+ <div
52
+ className="flex items-center justify-between gap-3 px-4 py-2 text-xs"
53
+ style={{ background: 'var(--amber-subtle, rgba(200,135,30,0.08))', borderBottom: '1px solid var(--border)' }}
54
+ >
55
+ <div className="flex items-center gap-2 min-w-0">
56
+ <span className="font-medium" style={{ color: 'var(--amber)' }}>
57
+ {updateT?.newVersion
58
+ ? updateT.newVersion(info.latest, info.current)
59
+ : `MindOS v${info.latest} available (current: v${info.current})`}
60
+ </span>
61
+ <span className="text-muted-foreground">
62
+ {updateT?.runUpdate ?? 'Run'}{' '}
63
+ <code className="px-1 py-0.5 rounded bg-muted font-mono text-[11px]">mindos update</code>
64
+ {updateT?.orSee ? (
65
+ <>
66
+ {' '}{updateT.orSee}{' '}
67
+ <a
68
+ href="https://github.com/GeminiLight/mindos/releases"
69
+ target="_blank"
70
+ rel="noopener noreferrer"
71
+ className="underline hover:text-foreground transition-colors"
72
+ >
73
+ {updateT.releaseNotes}
74
+ </a>
75
+ </>
76
+ ) : (
77
+ <>
78
+ {' '}or{' '}
79
+ <a
80
+ href="https://github.com/GeminiLight/mindos/releases"
81
+ target="_blank"
82
+ rel="noopener noreferrer"
83
+ className="underline hover:text-foreground transition-colors"
84
+ >
85
+ view release notes
86
+ </a>
87
+ </>
88
+ )}
89
+ </span>
90
+ </div>
91
+ <button
92
+ onClick={handleDismiss}
93
+ className="p-0.5 rounded hover:bg-muted transition-colors shrink-0"
94
+ style={{ color: 'var(--muted-foreground)' }}
95
+ title="Dismiss"
96
+ >
97
+ <X size={14} />
98
+ </button>
99
+ </div>
100
+ );
101
+ }
@@ -128,10 +128,10 @@ function OpCard({ op }: { op: AgentOp }) {
128
128
  onClick={() => setExpanded(v => !v)}
129
129
  >
130
130
  {/* kind badge */}
131
- <span style={{
131
+ <span className="font-display" style={{
132
132
  display: 'inline-flex', alignItems: 'center', gap: 4,
133
133
  padding: '2px 8px', borderRadius: 999, fontSize: '0.68rem',
134
- fontFamily: "'IBM Plex Mono',monospace", fontWeight: 600,
134
+ fontWeight: 600,
135
135
  background: style.bg, color: style.text, border: `1px solid ${style.border}`,
136
136
  flexShrink: 0,
137
137
  }}>
@@ -140,16 +140,17 @@ function OpCard({ op }: { op: AgentOp }) {
140
140
  </span>
141
141
 
142
142
  {/* tool name */}
143
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.78rem', color: 'var(--foreground)', fontWeight: 600, flexShrink: 0 }}>
143
+ <span className="font-display" style={{ fontSize: '0.78rem', color: 'var(--foreground)', fontWeight: 600, flexShrink: 0 }}>
144
144
  {toolShort}
145
145
  </span>
146
146
 
147
147
  {/* file path */}
148
148
  {filePath && (
149
149
  <span
150
- style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem', color: 'var(--amber)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }}
150
+ style={{ fontSize: '0.72rem', color: 'var(--amber)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }}
151
151
  onClick={e => { e.stopPropagation(); router.push('/view/' + filePath.split('/').map(encodeURIComponent).join('/')); }}
152
152
  title={filePath}
153
+ className="font-display"
153
154
  >
154
155
  {filePath}
155
156
  </span>
@@ -162,7 +163,7 @@ function OpCard({ op }: { op: AgentOp }) {
162
163
  : <AlertCircle size={13} style={{ color: '#c85050' }} />
163
164
  }
164
165
  {/* timestamp */}
165
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem', color: 'var(--muted-foreground)', opacity: 0.6 }} title={formatTs(op.ts)}>
166
+ <span className="font-display" style={{ fontSize: '0.68rem', color: 'var(--muted-foreground)', opacity: 0.6 }} title={formatTs(op.ts)}>
166
167
  {relativeTs(op.ts)}
167
168
  </span>
168
169
  {/* chevron */}
@@ -177,10 +178,10 @@ function OpCard({ op }: { op: AgentOp }) {
177
178
  <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: op.message ? 8 : 0 }}>
178
179
  {Object.entries(op.params).map(([k, v]) => (
179
180
  <div key={k} style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
180
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem', color: 'var(--muted-foreground)', opacity: 0.7, flexShrink: 0, minWidth: 80 }}>
181
+ <span className="font-display" style={{ fontSize: '0.68rem', color: 'var(--muted-foreground)', opacity: 0.7, flexShrink: 0, minWidth: 80 }}>
181
182
  {k}
182
183
  </span>
183
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem', color: 'var(--foreground)', wordBreak: 'break-all', lineHeight: 1.5 }}>
184
+ <span className="font-display" style={{ fontSize: '0.72rem', color: 'var(--foreground)', wordBreak: 'break-all', lineHeight: 1.5 }}>
184
185
  {truncateContent(v)}
185
186
  </span>
186
187
  </div>
@@ -188,7 +189,7 @@ function OpCard({ op }: { op: AgentOp }) {
188
189
  </div>
189
190
  {/* result message */}
190
191
  {op.message && (
191
- <div style={{ marginTop: 6, padding: '5px 9px', borderRadius: 5, fontSize: '0.72rem', fontFamily: "'IBM Plex Mono',monospace",
192
+ <div className="font-display" style={{ marginTop: 6, padding: '5px 9px', borderRadius: 5, fontSize: '0.72rem',
192
193
  background: op.result === 'error' ? 'rgba(200,80,80,0.08)' : 'rgba(122,173,128,0.08)',
193
194
  color: op.result === 'error' ? '#c85050' : '#7aad80',
194
195
  border: `1px solid ${op.result === 'error' ? 'rgba(200,80,80,0.2)' : 'rgba(122,173,128,0.2)'}`,
@@ -197,7 +198,7 @@ function OpCard({ op }: { op: AgentOp }) {
197
198
  </div>
198
199
  )}
199
200
  {/* absolute timestamp */}
200
- <div style={{ marginTop: 6, fontSize: '0.65rem', fontFamily: "'IBM Plex Mono',monospace", color: 'var(--muted-foreground)', opacity: 0.5 }}>
201
+ <div className="font-display" style={{ marginTop: 6, fontSize: '0.65rem', color: 'var(--muted-foreground)', opacity: 0.5 }}>
201
202
  {formatTs(op.ts)}
202
203
  </div>
203
204
  </div>
@@ -231,7 +232,7 @@ export function AgentInspectorRenderer({ content }: RendererContext) {
231
232
 
232
233
  if (ops.length === 0) {
233
234
  return (
234
- <div style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
235
+ <div className="font-display" style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 12 }}>
235
236
  <Terminal size={28} style={{ margin: '0 auto 10px', opacity: 0.3 }} />
236
237
  <p>No agent operations logged yet.</p>
237
238
  <p style={{ marginTop: 6, opacity: 0.6, fontSize: 11 }}>
@@ -254,10 +255,11 @@ export function AgentInspectorRenderer({ content }: RendererContext) {
254
255
  <button
255
256
  key={k}
256
257
  onClick={() => setFilter(k)}
258
+ className="font-display"
257
259
  style={{
258
260
  display: 'inline-flex', alignItems: 'center', gap: 4,
259
261
  padding: '3px 10px', borderRadius: 999, fontSize: '0.7rem',
260
- fontFamily: "'IBM Plex Mono',monospace", cursor: 'pointer', border: 'none',
262
+ cursor: 'pointer', border: 'none',
261
263
  background: active ? (style?.bg ?? 'var(--accent)') : 'var(--muted)',
262
264
  color: active ? (style?.text ?? 'var(--foreground)') : 'var(--muted-foreground)',
263
265
  outline: active ? `1px solid ${style?.border ?? 'var(--border)'}` : 'none',
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'agent-inspector',
5
+ name: 'Agent Inspector',
6
+ description: 'Visualizes agent tool-call logs as a filterable timeline. Auto-activates on .agent-log.json (JSON Lines format).',
7
+ author: 'MindOS',
8
+ icon: '🔍',
9
+ tags: ['agent', 'inspector', 'log', 'mcp', 'tools'],
10
+ builtin: true,
11
+ entryPath: '.agent-log.json',
12
+ match: ({ filePath }) => /\.agent-log\.json$/i.test(filePath),
13
+ load: () => import('./AgentInspectorRenderer').then(m => ({ default: m.AgentInspectorRenderer })),
14
+ };
@@ -51,7 +51,7 @@ export function BacklinksRenderer({ filePath }: RendererContext) {
51
51
 
52
52
  if (loading) {
53
53
  return (
54
- <div style={{ padding: '3rem 1rem', textAlign: 'center', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12, color: 'var(--muted-foreground)' }}>
54
+ <div className="font-display" style={{ padding: '3rem 1rem', textAlign: 'center', fontSize: 12, color: 'var(--muted-foreground)' }}>
55
55
  Scanning backlinks…
56
56
  </div>
57
57
  );
@@ -63,7 +63,7 @@ export function BacklinksRenderer({ filePath }: RendererContext) {
63
63
  <div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
64
64
  {/* header */}
65
65
  <div style={{ marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: 8 }}>
66
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
66
+ <span className="font-display" style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>
67
67
  {items.length === 0 ? 'No backlinks found' : `${items.length} file${items.length === 1 ? '' : 's'} link here`}
68
68
  </span>
69
69
  </div>
@@ -78,7 +78,7 @@ export function BacklinksRenderer({ filePath }: RendererContext) {
78
78
  fontSize: 13,
79
79
  }}>
80
80
  <FileText size={28} style={{ margin: '0 auto 10px', opacity: 0.3 }} />
81
- <p style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
81
+ <p className="font-display" style={{ fontSize: 12 }}>
82
82
  No other files link to <strong style={{ color: 'var(--foreground)' }}>{basename(filePath)}</strong> yet.
83
83
  </p>
84
84
  </div>
@@ -112,11 +112,11 @@ export function BacklinksRenderer({ filePath }: RendererContext) {
112
112
  background: 'var(--muted)',
113
113
  }}>
114
114
  <FileText size={13} style={{ color: 'var(--muted-foreground)', flexShrink: 0 }} />
115
- <span style={{ fontFamily: "'IBM Plex Sans',sans-serif", fontWeight: 600, fontSize: '0.85rem', color: 'var(--foreground)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
115
+ <span style={{ fontWeight: 600, fontSize: '0.85rem', color: 'var(--foreground)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
116
116
  {name}
117
117
  </span>
118
118
  {dir && (
119
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem', color: 'var(--muted-foreground)', opacity: 0.6, flexShrink: 0 }}>
119
+ <span className="font-display" style={{ fontSize: '0.68rem', color: 'var(--muted-foreground)', opacity: 0.6, flexShrink: 0 }}>
120
120
  {dir}
121
121
  </span>
122
122
  )}
@@ -131,7 +131,7 @@ export function BacklinksRenderer({ filePath }: RendererContext) {
131
131
  background: 'var(--background)',
132
132
  }}>
133
133
  {snippet.split('\n').map((line: string, j: number) => (
134
- <div key={j} style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem', color: 'var(--muted-foreground)', lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
134
+ <div key={j} className="font-display" style={{ fontSize: '0.72rem', color: 'var(--muted-foreground)', lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
135
135
  <SnippetLine text={line} />
136
136
  </div>
137
137
  ))}
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'backlinks',
5
+ name: 'Backlinks Explorer',
6
+ description: 'Shows all files that link to the current page via wikilinks or markdown links, with highlighted snippet context.',
7
+ author: 'MindOS',
8
+ icon: '🔗',
9
+ tags: ['backlinks', 'wiki', 'links', 'references'],
10
+ builtin: true,
11
+ entryPath: 'BACKLINKS.md',
12
+ match: ({ filePath }) => /\bBACKLINKS\b.*\.md$/i.test(filePath),
13
+ load: () => import('./BacklinksRenderer').then(m => ({ default: m.BacklinksRenderer })),
14
+ };
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'config-panel',
5
+ name: 'Config Panel',
6
+ description: 'Renders CONFIG.json as an editable control panel based on uiSchema/keySpecs. Changes are written back to the JSON file directly.',
7
+ author: 'MindOS',
8
+ icon: '🧩',
9
+ tags: ['config', 'json', 'settings', 'schema'],
10
+ builtin: true,
11
+ entryPath: 'CONFIG.json',
12
+ match: ({ filePath, extension }) => extension === 'json' && /(^|\/)CONFIG\.json$/i.test(filePath),
13
+ load: () => import('./ConfigRenderer').then(m => ({ default: m.ConfigRenderer })),
14
+ };
@@ -50,7 +50,7 @@ export function BoardView({ headers, rows, cfg, saveAction }: {
50
50
  <div className="flex-shrink-0 w-64 flex flex-col gap-2">
51
51
  <div className="flex items-center gap-2 px-1 py-1.5">
52
52
  <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: tc.text }} />
53
- <span className="text-xs font-semibold uppercase tracking-wider truncate" style={{ color: tc.text, fontFamily: "'IBM Plex Mono',monospace" }}>{group}</span>
53
+ <span className="text-xs font-semibold uppercase tracking-wider truncate font-display" style={{ color: tc.text }}>{group}</span>
54
54
  <span className="text-xs ml-auto shrink-0" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>{cards.length}</span>
55
55
  </div>
56
56
  <div
@@ -74,14 +74,14 @@ export function BoardView({ headers, rows, cfg, saveAction }: {
74
74
  className="rounded-lg border p-3 flex flex-col gap-1.5 cursor-grab active:cursor-grabbing hover:bg-muted/50 transition-colors"
75
75
  style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
76
76
  >
77
- <p className="text-sm font-medium leading-snug" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans',sans-serif" }}>{title}</p>
77
+ <p className="text-sm font-medium leading-snug" style={{ color: 'var(--foreground)' }}>{title}</p>
78
78
  {desc && <p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>{desc}</p>}
79
79
  <div className="flex flex-wrap gap-1 mt-0.5">
80
80
  {headers.map((h, ci) => {
81
81
  if (ci === groupIdx || ci === titleIdx || ci === descIdx) return null;
82
82
  const v = row[ci]; if (!v) return null;
83
- return <span key={ci} className="text-[10px] px-1.5 py-0.5 rounded"
84
- style={{ background: 'var(--muted)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
83
+ return <span key={ci} className="text-[10px] px-1.5 py-0.5 rounded font-display"
84
+ style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}
85
85
  >{h}: {v}</span>;
86
86
  })}
87
87
  </div>
@@ -115,27 +115,27 @@ export function BoardView({ headers, rows, cfg, saveAction }: {
115
115
  if (e.key === 'Escape') { setNewColInput(''); setShowNewCol(false); }
116
116
  }}
117
117
  placeholder="Column name…"
118
- className="text-xs bg-transparent outline-none w-full"
119
- style={{ color: 'var(--foreground)', borderBottom: '1px solid var(--amber)', fontFamily: "'IBM Plex Mono',monospace" }}
118
+ className="text-xs bg-transparent outline-none w-full font-display"
119
+ style={{ color: 'var(--foreground)', borderBottom: '1px solid var(--amber)' }}
120
120
  />
121
121
  <div className="flex gap-2">
122
122
  <button onClick={() => {
123
123
  setNewColInput('');
124
124
  setShowNewCol(false);
125
125
  }}
126
- className="text-xs px-2 py-1 rounded"
127
- style={{ background: 'var(--amber)', color: '#131210', fontFamily: "'IBM Plex Mono',monospace" }}
126
+ className="text-xs px-2 py-1 rounded font-display"
127
+ style={{ background: 'var(--amber)', color: '#131210' }}
128
128
  >Create</button>
129
129
  <button onClick={() => { setNewColInput(''); setShowNewCol(false); }}
130
- className="text-xs px-2 py-1 rounded"
131
- style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
130
+ className="text-xs px-2 py-1 rounded font-display"
131
+ style={{ color: 'var(--muted-foreground)' }}
132
132
  >Cancel</button>
133
133
  </div>
134
134
  </div>
135
135
  ) : (
136
136
  <button onClick={() => setShowNewCol(true)}
137
- className="flex items-center gap-1.5 text-xs px-3 py-2 rounded-xl border border-dashed w-full transition-colors hover:bg-muted"
138
- style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
137
+ className="flex items-center gap-1.5 text-xs px-3 py-2 rounded-xl border border-dashed w-full transition-colors hover:bg-muted font-display"
138
+ style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}
139
139
  >
140
140
  <Plus size={12} /> Add column
141
141
  </button>
@@ -10,15 +10,15 @@ export function ConfigPanel({ headers, cfg, view, onClose, onChange }: {
10
10
  onClose: () => void;
11
11
  onChange: (cfg: CsvConfig) => void;
12
12
  }) {
13
- const labelStyle: React.CSSProperties = { color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem' };
14
- const selectStyle: React.CSSProperties = { background: 'var(--background)', color: 'var(--foreground)', borderColor: 'var(--border)', fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem' };
13
+ const labelStyle: React.CSSProperties = { color: 'var(--muted-foreground)', fontSize: '0.72rem' };
14
+ const selectStyle: React.CSSProperties = { background: 'var(--background)', color: 'var(--foreground)', borderColor: 'var(--border)', fontSize: '0.72rem' };
15
15
 
16
16
  function FieldSelect({ label, value, onChange: onCh }: { label: string; value: string; onChange: (v: string) => void }) {
17
17
  return (
18
18
  <div className="flex items-center justify-between gap-2">
19
- <span style={labelStyle}>{label}</span>
19
+ <span className="font-display" style={labelStyle}>{label}</span>
20
20
  <select value={value} onChange={e => onCh(e.target.value)}
21
- className="rounded px-2 py-1 outline-none border" style={selectStyle}
21
+ className="rounded px-2 py-1 outline-none border font-display" style={selectStyle}
22
22
  >
23
23
  <option value="">— none —</option>
24
24
  {headers.map(h => <option key={h} value={h}>{h}</option>)}
@@ -47,9 +47,9 @@ export function ConfigPanel({ headers, cfg, view, onClose, onChange }: {
47
47
  <div className="flex rounded overflow-hidden border" style={{ borderColor: 'var(--border)' }}>
48
48
  {(['asc', 'desc'] as const).map(d => (
49
49
  <button key={d} onClick={() => onChange({ ...cfg, table: { ...cfg.table, sortDir: d } })}
50
- className="px-3 py-1 text-xs transition-colors"
50
+ className="px-3 py-1 text-xs transition-colors font-display"
51
51
  style={{
52
- fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem',
52
+ fontSize: '0.72rem',
53
53
  background: cfg.table.sortDir === d ? 'var(--amber)' : 'var(--background)',
54
54
  color: cfg.table.sortDir === d ? '#131210' : 'var(--muted-foreground)',
55
55
  }}
@@ -77,9 +77,8 @@ export function ConfigPanel({ headers, cfg, view, onClose, onChange }: {
77
77
  : [...cfg.table.hiddenFields, h];
78
78
  onChange({ ...cfg, table: { ...cfg.table, hiddenFields: next } });
79
79
  }}
80
- className="text-[11px] px-2 py-0.5 rounded transition-colors"
80
+ className="text-[11px] px-2 py-0.5 rounded transition-colors font-display"
81
81
  style={{
82
- fontFamily: "'IBM Plex Mono',monospace",
83
82
  background: hidden ? 'var(--muted)' : 'var(--amber-dim)',
84
83
  color: hidden ? 'var(--muted-foreground)' : 'var(--amber)',
85
84
  }}
@@ -3,12 +3,12 @@
3
3
  import { useState, useMemo, useCallback, useEffect } from 'react';
4
4
  import { LayoutGrid, Columns, Table2, Settings2 } from 'lucide-react';
5
5
  import type { RendererContext } from '@/lib/renderers/registry';
6
- import type { ViewType, CsvConfig } from './csv/types';
7
- import { defaultConfig, loadConfig, saveConfig, parseCSV } from './csv/types';
8
- import { TableView } from './csv/TableView';
9
- import { GalleryView } from './csv/GalleryView';
10
- import { BoardView } from './csv/BoardView';
11
- import { ConfigPanel } from './csv/ConfigPanel';
6
+ import type { ViewType, CsvConfig } from './types';
7
+ import { defaultConfig, loadConfig, saveConfig, parseCSV } from './types';
8
+ import { TableView } from './TableView';
9
+ import { GalleryView } from './GalleryView';
10
+ import { BoardView } from './BoardView';
11
+ import { ConfigPanel } from './ConfigPanel';
12
12
 
13
13
  const VIEW_TABS: { id: ViewType; icon: React.ReactNode; label: string }[] = [
14
14
  { id: 'table', icon: <Table2 size={13} />, label: 'Table' },
@@ -42,9 +42,8 @@ export function CsvRenderer({ filePath, content, saveAction }: RendererContext)
42
42
  <div className="flex items-center gap-0.5 p-1 rounded-lg" style={{ background: 'var(--muted)' }}>
43
43
  {VIEW_TABS.map(tab => (
44
44
  <button key={tab.id} onClick={() => updateConfig({ ...cfg, activeView: tab.id })}
45
- className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors"
45
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors font-display"
46
46
  style={{
47
- fontFamily: "'IBM Plex Mono',monospace",
48
47
  background: view === tab.id ? 'var(--card)' : 'transparent',
49
48
  color: view === tab.id ? 'var(--foreground)' : 'var(--muted-foreground)',
50
49
  boxShadow: view === tab.id ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
@@ -53,7 +52,7 @@ export function CsvRenderer({ filePath, content, saveAction }: RendererContext)
53
52
  ))}
54
53
  </div>
55
54
  <div className="flex-1" />
56
- <span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", opacity: 0.5 }}>
55
+ <span className="text-xs font-display" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>
57
56
  {rows.length} rows
58
57
  </span>
59
58
  <div className="relative">
@@ -19,15 +19,15 @@ export function GalleryView({ headers, rows, cfg }: { headers: string[]; rows: s
19
19
  style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
20
20
  >
21
21
  {tag && tc && <span className="self-start text-[11px] px-2 py-0.5 rounded-full font-medium"
22
- style={{ background: tc.bg, color: tc.text, fontFamily: "'IBM Plex Mono',monospace" }}>{tag}</span>}
23
- <p className="text-sm font-semibold leading-snug" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans',sans-serif" }}>{title}</p>
22
+ style={{ background: tc.bg, color: tc.text }}>{tag}</span>}
23
+ <p className="text-sm font-semibold leading-snug" style={{ color: 'var(--foreground)' }}>{title}</p>
24
24
  {desc && <p className="text-xs leading-relaxed line-clamp-3" style={{ color: 'var(--muted-foreground)' }}>{desc}</p>}
25
25
  <div className="mt-1 flex flex-col gap-0.5">
26
26
  {headers.map((h, ci) => {
27
27
  if (ci === titleIdx || ci === descIdx || ci === tagIdx) return null;
28
28
  const v = row[ci]; if (!v) return null;
29
29
  return <div key={ci} className="flex items-baseline gap-1.5 text-xs">
30
- <span style={{ color: 'var(--muted-foreground)', opacity: 0.6, fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem' }}>{h}</span>
30
+ <span className="font-display" style={{ color: 'var(--muted-foreground)', opacity: 0.6, fontSize: '0.68rem' }}>{h}</span>
31
31
  <span className="truncate" style={{ color: 'var(--muted-foreground)' }}>{v}</span>
32
32
  </div>;
33
33
  })}
@@ -71,7 +71,6 @@ export function TableView({ headers, rows, cfg, saveAction }: {
71
71
 
72
72
  const thStyle: React.CSSProperties = {
73
73
  borderBottom: '1px solid var(--border)',
74
- fontFamily: "'IBM Plex Sans',sans-serif",
75
74
  fontSize: '0.72rem',
76
75
  letterSpacing: '0.05em',
77
76
  textTransform: 'uppercase',
@@ -110,7 +109,7 @@ export function TableView({ headers, rows, cfg, saveAction }: {
110
109
  <td colSpan={visibleIndices.length + 1} className="px-4 py-1.5"
111
110
  style={{ background: 'var(--accent)', borderBottom: '1px solid var(--border)', borderTop: '1px solid var(--border)' }}
112
111
  >
113
- <span className="text-xs font-semibold" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}>
112
+ <span className="text-xs font-semibold font-display" style={{ color: 'var(--muted-foreground)' }}>
114
113
  {section.key} · {section.rows.length}
115
114
  </span>
116
115
  </td>
@@ -147,15 +146,15 @@ export function TableView({ headers, rows, cfg, saveAction }: {
147
146
  </table>
148
147
  </div>
149
148
  <div className="px-4 py-2 flex items-center justify-between" style={{ background: 'var(--muted)', borderTop: '1px solid var(--border)' }}>
150
- <span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}>
149
+ <span className="text-xs font-display" style={{ color: 'var(--muted-foreground)' }}>
151
150
  {localRows.length} rows · {headers.length} cols
152
151
  </span>
153
152
  {!showAdd
154
153
  ? <button onClick={() => setShowAdd(true)} className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md"
155
- style={{ color: 'var(--amber)', background: 'var(--amber-dim)', fontFamily: "'IBM Plex Mono',monospace" }}
154
+ style={{ color: 'var(--amber)', background: 'var(--amber-dim)' }}
156
155
  ><Plus size={12} /> Add row</button>
157
156
  : <button onClick={() => setShowAdd(false)} className="text-xs px-2.5 py-1 rounded-md"
158
- style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
157
+ style={{ color: 'var(--muted-foreground)' }}
159
158
  >Cancel</button>
160
159
  }
161
160
  </div>
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'csv',
5
+ name: 'CSV Views',
6
+ description: 'Renders any CSV file as Table, Gallery, or Board. Each view is independently configurable — choose which columns map to title, description, tag, and group.',
7
+ author: 'MindOS',
8
+ icon: '📊',
9
+ tags: ['csv', 'table', 'gallery', 'board', 'data'],
10
+ builtin: true,
11
+ entryPath: 'Resources/Products.csv',
12
+ match: ({ extension, filePath }) => extension === 'csv' && !/\bTODO\b/i.test(filePath),
13
+ load: () => import('./CsvRenderer').then(m => ({ default: m.CsvRenderer })),
14
+ };
@@ -182,7 +182,8 @@ function DiffCard({ entry, saveAction, fullContent }: {
182
182
  <div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '10px 14px', borderBottom: expanded ? '1px solid var(--border)' : 'none' }}>
183
183
  <FileEdit size={13} style={{ color: 'var(--amber)', flexShrink: 0 }} />
184
184
  <span
185
- style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.78rem', color: 'var(--amber)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }}
185
+ className="font-display"
186
+ style={{ fontSize: '0.78rem', color: 'var(--amber)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }}
186
187
  onClick={() => router.push('/view/' + entry.path.split('/').map(encodeURIComponent).join('/'))}
187
188
  title={entry.path}
188
189
  >
@@ -190,16 +191,16 @@ function DiffCard({ entry, saveAction, fullContent }: {
190
191
  </span>
191
192
 
192
193
  {/* diff stats */}
193
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: '#7aad80', flexShrink: 0 }}>+{added}</span>
194
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: '#c85050', flexShrink: 0 }}>−{removed}</span>
194
+ <span className="font-display" style={{ fontSize: '0.7rem', color: '#7aad80', flexShrink: 0 }}>+{added}</span>
195
+ <span className="font-display" style={{ fontSize: '0.7rem', color: '#c85050', flexShrink: 0 }}>−{removed}</span>
195
196
 
196
197
  {/* tool badge */}
197
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.65rem', padding: '1px 7px', borderRadius: 999, background: 'var(--muted)', color: 'var(--muted-foreground)', flexShrink: 0 }}>
198
+ <span className="font-display" style={{ fontSize: '0.65rem', padding: '1px 7px', borderRadius: 999, background: 'var(--muted)', color: 'var(--muted-foreground)', flexShrink: 0 }}>
198
199
  {toolShort}
199
200
  </span>
200
201
 
201
202
  {/* timestamp */}
202
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.65rem', color: 'var(--muted-foreground)', opacity: 0.6, flexShrink: 0 }}>
203
+ <span className="font-display" style={{ fontSize: '0.65rem', color: 'var(--muted-foreground)', opacity: 0.6, flexShrink: 0 }}>
203
204
  {relativeTs(entry.ts)}
204
205
  </span>
205
206
 
@@ -222,7 +223,7 @@ function DiffCard({ entry, saveAction, fullContent }: {
222
223
  </button>
223
224
  </>
224
225
  ) : (
225
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem', color: approved ? '#7aad80' : '#c85050' }}>
226
+ <span className="font-display" style={{ fontSize: '0.68rem', color: approved ? '#7aad80' : '#c85050' }}>
226
227
  {approved ? '✓ approved' : '✕ reverted'}
227
228
  </span>
228
229
  )}
@@ -237,7 +238,7 @@ function DiffCard({ entry, saveAction, fullContent }: {
237
238
 
238
239
  {/* diff view */}
239
240
  {expanded && (
240
- <div style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem', lineHeight: 1.5, overflowX: 'auto' }}>
241
+ <div className="font-display" style={{ fontSize: '0.72rem', lineHeight: 1.5, overflowX: 'auto' }}>
241
242
  {collapsed.map((line, i) => {
242
243
  if (line.type === 'collapse') {
243
244
  return (
@@ -280,7 +281,7 @@ export function DiffRenderer({ content, saveAction }: RendererContext) {
280
281
 
281
282
  if (entries.length === 0) {
282
283
  return (
283
- <div style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
284
+ <div className="font-display" style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 12 }}>
284
285
  <GitCompare size={28} style={{ margin: '0 auto 10px', opacity: 0.3 }} />
285
286
  <p>No agent diffs logged yet.</p>
286
287
  <p style={{ marginTop: 6, opacity: 0.6, fontSize: 11 }}>
@@ -296,7 +297,7 @@ export function DiffRenderer({ content, saveAction }: RendererContext) {
296
297
  return (
297
298
  <div style={{ maxWidth: 800, margin: '0 auto', padding: '1.5rem 0' }}>
298
299
  {/* stats bar */}
299
- <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: '1.2rem', fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
300
+ <div className="font-display" style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: '1.2rem', fontSize: 11, color: 'var(--muted-foreground)' }}>
300
301
  <span>{entries.length} change{entries.length !== 1 ? 's' : ''}</span>
301
302
  <span style={{ color: '#7aad80' }}>+{totalAdded}</span>
302
303
  <span style={{ color: '#c85050' }}>−{totalRemoved}</span>
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'diff-viewer',
5
+ name: 'Diff Viewer',
6
+ description: 'Visualizes agent file changes as a side-by-side diff timeline. Auto-activates on Agent-Diff.md with embedded agent-diff blocks.',
7
+ author: 'MindOS',
8
+ icon: '📝',
9
+ tags: ['diff', 'agent', 'changes', 'history'],
10
+ builtin: true,
11
+ entryPath: 'Agent-Diff.md',
12
+ match: ({ filePath }) => /\bAgent-Diff\b.*\.md$/i.test(filePath),
13
+ load: () => import('./DiffRenderer').then(m => ({ default: m.DiffRenderer })),
14
+ };