@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.
- package/app/app/api/mcp/agents/route.ts +72 -0
- package/app/app/api/mcp/install/route.ts +95 -0
- package/app/app/api/mcp/status/route.ts +47 -0
- package/app/app/api/skills/route.ts +208 -0
- package/app/app/api/sync/route.ts +54 -3
- package/app/app/api/update-check/route.ts +52 -0
- package/app/app/globals.css +12 -0
- package/app/app/layout.tsx +4 -2
- package/app/app/login/page.tsx +20 -13
- package/app/app/page.tsx +17 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
- package/app/app/view/[...path]/loading.tsx +1 -1
- package/app/app/view/[...path]/not-found.tsx +101 -0
- package/app/components/AskFab.tsx +1 -1
- package/app/components/AskModal.tsx +1 -1
- package/app/components/Backlinks.tsx +1 -1
- package/app/components/Breadcrumb.tsx +13 -3
- package/app/components/CsvView.tsx +5 -6
- package/app/components/DirView.tsx +42 -21
- package/app/components/FindInPage.tsx +211 -0
- package/app/components/HomeContent.tsx +97 -44
- package/app/components/JsonView.tsx +1 -2
- package/app/components/MarkdownEditor.tsx +1 -2
- package/app/components/OnboardingView.tsx +6 -7
- package/app/components/SettingsModal.tsx +5 -2
- package/app/components/SetupWizard.tsx +4 -4
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/UpdateBanner.tsx +101 -0
- package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
- package/app/components/renderers/agent-inspector/manifest.ts +14 -0
- package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
- package/app/components/renderers/backlinks/manifest.ts +14 -0
- package/app/components/renderers/config/manifest.ts +14 -0
- package/app/components/renderers/csv/BoardView.tsx +12 -12
- package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
- package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
- package/app/components/renderers/csv/GalleryView.tsx +3 -3
- package/app/components/renderers/csv/TableView.tsx +4 -5
- package/app/components/renderers/csv/manifest.ts +14 -0
- package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
- package/app/components/renderers/diff/manifest.ts +14 -0
- package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
- package/app/components/renderers/graph/manifest.ts +14 -0
- package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
- package/app/components/renderers/summary/manifest.ts +14 -0
- package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
- package/app/components/renderers/timeline/manifest.ts +14 -0
- package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
- package/app/components/renderers/todo/manifest.ts +14 -0
- package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
- package/app/components/renderers/workflow/manifest.ts +14 -0
- package/app/components/settings/McpTab.tsx +549 -0
- package/app/components/settings/SyncTab.tsx +139 -50
- package/app/components/settings/types.ts +1 -1
- package/app/data/pages/home.png +0 -0
- package/app/lib/i18n.ts +178 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +3 -0
- package/app/package.json +1 -0
- package/app/types/semver.d.ts +8 -0
- package/bin/cli.js +137 -24
- package/bin/lib/build.js +53 -18
- package/bin/lib/colors.js +3 -1
- package/bin/lib/config.js +4 -0
- package/bin/lib/constants.js +2 -0
- package/bin/lib/debug.js +10 -0
- package/bin/lib/startup.js +21 -20
- package/bin/lib/stop.js +41 -3
- package/bin/lib/sync.js +65 -53
- package/bin/lib/update-check.js +94 -0
- package/bin/lib/utils.js +2 -2
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +57 -0
- package/scripts/setup.js +24 -0
- /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
|
-
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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',
|
|
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',
|
|
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)',
|
|
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
|
-
|
|
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',
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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
|
|
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)'
|
|
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)'
|
|
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)'
|
|
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'
|
|
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)'
|
|
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)'
|
|
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)',
|
|
14
|
-
const selectStyle: React.CSSProperties = { background: 'var(--background)', color: 'var(--foreground)', borderColor: 'var(--border)',
|
|
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
|
-
|
|
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 './
|
|
7
|
-
import { defaultConfig, loadConfig, saveConfig, parseCSV } from './
|
|
8
|
-
import { TableView } from './
|
|
9
|
-
import { GalleryView } from './
|
|
10
|
-
import { BoardView } from './
|
|
11
|
-
import { ConfigPanel } from './
|
|
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)',
|
|
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
|
|
23
|
-
<p className="text-sm font-semibold leading-snug" style={{ color: 'var(--foreground)'
|
|
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,
|
|
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)'
|
|
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)'
|
|
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)'
|
|
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)'
|
|
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
|
-
|
|
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={{
|
|
194
|
-
<span style={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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)',
|
|
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',
|
|
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
|
+
};
|