@geminilight/mindos 0.5.8 → 0.5.10
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/README.md +9 -10
- package/README_zh.md +8 -9
- package/app/app/api/mcp/agents/route.ts +7 -0
- package/app/app/api/mcp/install-skill/route.ts +6 -0
- package/app/app/api/setup/check-port/route.ts +27 -3
- package/app/app/api/setup/route.ts +2 -9
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/globals.css +28 -4
- package/app/app/login/page.tsx +2 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +15 -10
- package/app/app/view/[...path]/not-found.tsx +1 -1
- package/app/components/AskModal.tsx +5 -5
- package/app/components/Breadcrumb.tsx +2 -2
- package/app/components/DirView.tsx +6 -6
- package/app/components/FileTree.tsx +7 -7
- package/app/components/HomeContent.tsx +8 -8
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/SearchModal.tsx +1 -1
- package/app/components/SettingsModal.tsx +2 -2
- package/app/components/SetupWizard.tsx +1 -1258
- package/app/components/Sidebar.tsx +4 -4
- package/app/components/SidebarLayout.tsx +9 -0
- package/app/components/SyncStatusBar.tsx +6 -6
- package/app/components/TableOfContents.tsx +1 -1
- package/app/components/UpdateBanner.tsx +1 -1
- package/app/components/ask/FileChip.tsx +1 -1
- package/app/components/ask/MentionPopover.tsx +4 -4
- package/app/components/ask/MessageList.tsx +3 -3
- package/app/components/ask/SessionHistory.tsx +3 -3
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
- package/app/components/renderers/config/ConfigRenderer.tsx +4 -4
- package/app/components/renderers/csv/BoardView.tsx +2 -2
- package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
- package/app/components/renderers/csv/GalleryView.tsx +1 -1
- package/app/components/renderers/csv/types.ts +1 -1
- package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
- package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
- package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
- package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +4 -4
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpTab.tsx +93 -47
- package/app/components/settings/PluginsTab.tsx +4 -4
- package/app/components/settings/Primitives.tsx +4 -4
- package/app/components/settings/SyncTab.tsx +13 -13
- package/app/components/setup/StepAI.tsx +67 -0
- package/app/components/setup/StepAgents.tsx +237 -0
- package/app/components/setup/StepDots.tsx +39 -0
- package/app/components/setup/StepKB.tsx +237 -0
- package/app/components/setup/StepPorts.tsx +121 -0
- package/app/components/setup/StepReview.tsx +211 -0
- package/app/components/setup/StepSecurity.tsx +78 -0
- package/app/components/setup/constants.tsx +13 -0
- package/app/components/setup/index.tsx +464 -0
- package/app/components/setup/types.ts +53 -0
- package/app/lib/i18n.ts +52 -8
- package/app/lib/mcp-agents.ts +81 -0
- package/bin/lib/gateway.js +44 -4
- package/bin/lib/mcp-agents.js +81 -0
- package/bin/lib/mcp-install.js +34 -4
- package/package.json +3 -1
- package/scripts/setup.js +43 -6
- package/skills/project-wiki/SKILL.md +92 -63
- package/app/public/landing/index.html +0 -353
- package/app/public/landing/style.css +0 -216
package/README.md
CHANGED
|
@@ -70,22 +70,21 @@ All that experience from your conversations — gone the moment you close the wi
|
|
|
70
70
|
|
|
71
71
|
**For Humans**
|
|
72
72
|
|
|
73
|
-
- **GUI
|
|
74
|
-
- **Built-in Agent Assistant**: converse in context
|
|
75
|
-
- **Plugin
|
|
73
|
+
- **GUI Workbench**: browse, edit, search notes with unified search + AI entry (`⌘K` / `⌘/`), designed for human-AI co-creation.
|
|
74
|
+
- **Built-in Agent Assistant**: converse with the knowledge base in context; edits seamlessly capture human-curated knowledge.
|
|
75
|
+
- **Plugin Extensions**: multiple built-in renderer plugins — TODO Board, CSV Views, Wiki Graph, Timeline, Agent Inspector, and more.
|
|
76
76
|
|
|
77
77
|
**For Agents**
|
|
78
78
|
|
|
79
|
-
- **MCP Server + Skills**:
|
|
80
|
-
- **Structured Templates**:
|
|
81
|
-
- **
|
|
79
|
+
- **MCP Server + Skills**: stdio + HTTP dual transport, full-lineup Agent compatible (OpenClaw, Claude Code, Cursor, etc.). Zero-config access.
|
|
80
|
+
- **Structured Templates**: pre-set directory structures for Profiles, Workflows, Configurations, etc., to jumpstart personal context.
|
|
81
|
+
- **Agent-Ready Docs**: everyday notes naturally double as high-quality executable Agent commands — no format conversion needed, write and dispatch.
|
|
82
82
|
|
|
83
83
|
**Infrastructure**
|
|
84
84
|
|
|
85
85
|
- **Security**: Bearer Token auth, path sandboxing, INSTRUCTION.md write-protection, atomic writes.
|
|
86
|
-
- **Knowledge Graph**:
|
|
87
|
-
- **Git Time Machine**:
|
|
88
|
-
- **Cross-Device Sync**: auto-commit, push, and pull via Git — edits on one device appear on all others within minutes.
|
|
86
|
+
- **Knowledge Graph**: dynamically parses and visualizes inter-file references and dependencies.
|
|
87
|
+
- **Git Time Machine**: Git auto-sync (commit/push/pull), records every edit by both humans and Agents. One-click rollback, cross-device sync.
|
|
89
88
|
|
|
90
89
|
<details>
|
|
91
90
|
<summary><strong>Coming Soon</strong></summary>
|
|
@@ -418,7 +417,7 @@ MindOS/
|
|
|
418
417
|
~/.mindos/ # User data directory (outside project, never committed)
|
|
419
418
|
├── config.json # All configuration (AI keys, port, auth token, sync settings)
|
|
420
419
|
├── sync-state.json # Sync state (last sync time, conflicts)
|
|
421
|
-
└──
|
|
420
|
+
└── mind/ # Your private knowledge base (default: ~/MindOS/mind, customizable on onboard)
|
|
422
421
|
```
|
|
423
422
|
|
|
424
423
|
---
|
package/README_zh.md
CHANGED
|
@@ -70,22 +70,21 @@ Agent 记了什么、记对没有,用户无从知晓。**MindOS 将每次读
|
|
|
70
70
|
|
|
71
71
|
**人类侧**
|
|
72
72
|
|
|
73
|
-
- **GUI
|
|
74
|
-
- **内置 Agent
|
|
75
|
-
-
|
|
73
|
+
- **GUI 工作台**:浏览、编辑、搜索笔记,统一搜索 + AI 入口(`⌘K` / `⌘/`),专为人机共创设计。
|
|
74
|
+
- **内置 Agent 助手**:在上下文中与知识库对话,编辑无缝沉淀为可管理知识。
|
|
75
|
+
- **插件扩展**:多种内置渲染器插件——TODO Board、CSV Views、Wiki Graph、Timeline、Agent Inspector 等。
|
|
76
76
|
|
|
77
77
|
**Agent 侧**
|
|
78
78
|
|
|
79
|
-
- **MCP Server + Skills
|
|
80
|
-
-
|
|
81
|
-
-
|
|
79
|
+
- **MCP Server + Skills**:stdio + HTTP 双传输,全阵容 Agent 兼容(OpenClaw, Claude Code, Cursor 等),零配置接入。
|
|
80
|
+
- **结构化模板**:预置 Profile、Workflows、Configurations 等目录骨架,快速冷启动个人 Context。
|
|
81
|
+
- **笔记即指令**:日常笔记天然就是 Agent 可直接执行的高质量指令——无需额外格式转换,写下即可调度。
|
|
82
82
|
|
|
83
83
|
**基础设施**
|
|
84
84
|
|
|
85
85
|
- **安全防线**:Bearer Token 认证、路径沙箱、INSTRUCTION.md 写保护、原子写入。
|
|
86
|
-
-
|
|
87
|
-
- **Git
|
|
88
|
-
- **跨设备同步**:通过 Git 自动 commit、push、pull —— 一台设备的编辑几分钟内同步到所有设备。
|
|
86
|
+
- **知识图谱**:动态解析并可视化文件间的引用与依赖关系。
|
|
87
|
+
- **Git 时光机**:Git 自动同步(commit/push/pull),记录人类与 Agent 的每次编辑历史,一键回滚,跨设备同步。
|
|
89
88
|
|
|
90
89
|
<details>
|
|
91
90
|
<summary><strong>即将到来</strong></summary>
|
|
@@ -20,6 +20,13 @@ export async function GET() {
|
|
|
20
20
|
preferredTransport: agent.preferredTransport,
|
|
21
21
|
};
|
|
22
22
|
});
|
|
23
|
+
|
|
24
|
+
// Sort: installed first, then detected, then not found
|
|
25
|
+
agents.sort((a, b) => {
|
|
26
|
+
const rank = (x: typeof a) => x.installed ? 0 : x.present ? 1 : 2;
|
|
27
|
+
return rank(a) - rank(b);
|
|
28
|
+
});
|
|
29
|
+
|
|
23
30
|
return NextResponse.json({ agents });
|
|
24
31
|
} catch (err) {
|
|
25
32
|
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
@@ -24,6 +24,12 @@ const AGENT_NAME_MAP: Record<string, string> = {
|
|
|
24
24
|
'trae': 'trae',
|
|
25
25
|
'openclaw': 'openclaw',
|
|
26
26
|
'codebuddy': 'codebuddy',
|
|
27
|
+
'iflow-cli': 'iflow-cli',
|
|
28
|
+
'pi': 'pi',
|
|
29
|
+
'augment': 'augment',
|
|
30
|
+
'qwen-code': 'qwen-code',
|
|
31
|
+
'trae-cn': 'trae-cn',
|
|
32
|
+
'roo': 'roo',
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
/* ── Helpers ──────────────────────────────────────────────────── */
|
|
@@ -27,29 +27,53 @@ async function isSelfPort(port: number): Promise<boolean> {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async function findFreePort(start: number): Promise<number | null> {
|
|
30
|
+
async function findFreePort(start: number, selfPorts: Set<number>): Promise<number | null> {
|
|
31
31
|
for (let p = start; p <= 65535; p++) {
|
|
32
|
+
if (selfPorts.has(p)) continue;
|
|
32
33
|
if (!await isPortInUse(p)) return p;
|
|
33
34
|
}
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The port this MindOS web server is actually listening on.
|
|
40
|
+
* Derived from the incoming request URL — always reliable, no network round-trip.
|
|
41
|
+
*
|
|
42
|
+
* Note: We intentionally do NOT read settings here. Settings contain *configured*
|
|
43
|
+
* ports (webPort / mcpPort), which may not actually be listening yet (e.g. during
|
|
44
|
+
* first onboard, or if MCP server hasn't started). Treating configured-but-not-
|
|
45
|
+
* listening ports as "self" would mask real conflicts.
|
|
46
|
+
*/
|
|
47
|
+
function getListeningPort(req: NextRequest): number {
|
|
48
|
+
return parseInt(req.nextUrl.port || '0', 10);
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
export async function POST(req: NextRequest) {
|
|
38
52
|
try {
|
|
39
53
|
const { port } = await req.json() as { port: number };
|
|
40
54
|
if (!port || port < 1024 || port > 65535) {
|
|
41
55
|
return NextResponse.json({ error: 'Invalid port' }, { status: 400 });
|
|
42
56
|
}
|
|
57
|
+
|
|
58
|
+
const myPort = getListeningPort(req);
|
|
59
|
+
|
|
60
|
+
// Fast path: if checking the port we're currently listening on, skip network round-trip
|
|
61
|
+
if (myPort > 0 && port === myPort) {
|
|
62
|
+
return NextResponse.json({ available: true, isSelf: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
const inUse = await isPortInUse(port);
|
|
44
66
|
if (!inUse) {
|
|
45
67
|
return NextResponse.json({ available: true, isSelf: false });
|
|
46
68
|
}
|
|
47
|
-
// Port is occupied — check if it's
|
|
69
|
+
// Port is occupied — check if it's another MindOS instance
|
|
48
70
|
const self = await isSelfPort(port);
|
|
49
71
|
if (self) {
|
|
50
72
|
return NextResponse.json({ available: true, isSelf: true });
|
|
51
73
|
}
|
|
52
|
-
const
|
|
74
|
+
const skipPorts = new Set<number>();
|
|
75
|
+
if (myPort > 0) skipPorts.add(myPort);
|
|
76
|
+
const suggestion = await findFreePort(port + 1, skipPorts);
|
|
53
77
|
return NextResponse.json({ available: false, isSelf: false, suggestion });
|
|
54
78
|
} catch (err) {
|
|
55
79
|
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
@@ -67,17 +67,10 @@ export async function POST(req: NextRequest) {
|
|
|
67
67
|
return NextResponse.json({ error: `Invalid MCP port: ${mcpPortNum}` }, { status: 400 });
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Apply template
|
|
70
|
+
// Apply template (copyRecursive has skip-existing protection)
|
|
71
71
|
const dirExists = fs.existsSync(resolvedRoot);
|
|
72
|
-
let dirEmpty = true;
|
|
73
|
-
if (dirExists) {
|
|
74
|
-
try {
|
|
75
|
-
const entries = fs.readdirSync(resolvedRoot).filter(e => !e.startsWith('.'));
|
|
76
|
-
dirEmpty = entries.length === 0;
|
|
77
|
-
} catch { /* treat as empty */ }
|
|
78
|
-
}
|
|
79
72
|
|
|
80
|
-
if (template
|
|
73
|
+
if (template) {
|
|
81
74
|
applyTemplate(template, resolvedRoot);
|
|
82
75
|
} else if (!dirExists) {
|
|
83
76
|
fs.mkdirSync(resolvedRoot, { recursive: true });
|
|
@@ -9,7 +9,7 @@ const PROJECT_ROOT = path.resolve(process.cwd(), '..');
|
|
|
9
9
|
|
|
10
10
|
function getMindRoot(): string {
|
|
11
11
|
const s = readSettings();
|
|
12
|
-
return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS');
|
|
12
|
+
return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS', 'mind');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
interface SkillInfo {
|
package/app/app/globals.css
CHANGED
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
--color-input: var(--input);
|
|
27
27
|
--color-border: var(--border);
|
|
28
28
|
--color-destructive: var(--destructive);
|
|
29
|
+
--color-success: var(--success);
|
|
30
|
+
--color-error: var(--error);
|
|
31
|
+
--color-amber-foreground: var(--amber-foreground);
|
|
29
32
|
--color-accent-foreground: var(--accent-foreground);
|
|
30
33
|
--color-accent: var(--accent);
|
|
31
34
|
--color-muted-foreground: var(--muted-foreground);
|
|
@@ -63,16 +66,19 @@ body {
|
|
|
63
66
|
--secondary: #e8e4db;
|
|
64
67
|
--secondary-foreground: #1c1a17;
|
|
65
68
|
--muted: #e8e4db;
|
|
66
|
-
--muted-foreground: #
|
|
69
|
+
--muted-foreground: #685f52;
|
|
67
70
|
--accent: #d9d3c6;
|
|
68
71
|
--accent-foreground: #1c1a17;
|
|
69
72
|
--destructive: oklch(0.58 0.22 27);
|
|
70
73
|
--border: rgba(28, 26, 23, 0.1);
|
|
71
74
|
--input: rgba(28, 26, 23, 0.12);
|
|
72
|
-
--ring:
|
|
75
|
+
--ring: var(--amber);
|
|
73
76
|
--radius: 0.5rem;
|
|
74
77
|
--amber: #c8873a;
|
|
75
78
|
--amber-dim: rgba(200, 135, 58, 0.12);
|
|
79
|
+
--amber-foreground: #131210;
|
|
80
|
+
--success: #7aad80;
|
|
81
|
+
--error: #c85050;
|
|
76
82
|
--sidebar: #ede9e1;
|
|
77
83
|
--sidebar-foreground: #1c1a17;
|
|
78
84
|
--sidebar-primary: #1c1a17;
|
|
@@ -101,9 +107,12 @@ body {
|
|
|
101
107
|
--destructive: oklch(0.704 0.191 22.216);
|
|
102
108
|
--border: rgba(232, 228, 220, 0.08);
|
|
103
109
|
--input: rgba(232, 228, 220, 0.1);
|
|
104
|
-
--ring:
|
|
110
|
+
--ring: var(--amber);
|
|
105
111
|
--amber: #d4954a;
|
|
106
112
|
--amber-dim: rgba(212, 149, 74, 0.12);
|
|
113
|
+
--amber-foreground: #131210;
|
|
114
|
+
--success: #7aad80;
|
|
115
|
+
--error: #c85050;
|
|
107
116
|
--sidebar: #1c1a17;
|
|
108
117
|
--sidebar-foreground: #e8e4dc;
|
|
109
118
|
--sidebar-primary: #d4954a;
|
|
@@ -276,6 +285,11 @@ body {
|
|
|
276
285
|
-webkit-backdrop-filter: blur(8px);
|
|
277
286
|
}
|
|
278
287
|
|
|
288
|
+
/* Micro type scale: text-2xs = 10px (between nothing and text-xs 12px) */
|
|
289
|
+
@layer utilities {
|
|
290
|
+
.text-2xs { font-size: 10px; line-height: 1.4; }
|
|
291
|
+
}
|
|
292
|
+
|
|
279
293
|
/* Hide scrollbar but keep scroll functionality */
|
|
280
294
|
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
|
|
281
295
|
.scrollbar-none::-webkit-scrollbar { display: none; }
|
|
@@ -290,6 +304,16 @@ body {
|
|
|
290
304
|
button, a { -webkit-tap-highlight-color: transparent; }
|
|
291
305
|
}
|
|
292
306
|
|
|
307
|
+
/* Respect user's reduced-motion preference */
|
|
308
|
+
@media (prefers-reduced-motion: reduce) {
|
|
309
|
+
*, *::before, *::after {
|
|
310
|
+
animation-duration: 0.01ms !important;
|
|
311
|
+
animation-iteration-count: 1 !important;
|
|
312
|
+
transition-duration: 0.01ms !important;
|
|
313
|
+
scroll-behavior: auto !important;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
293
317
|
/* Global focus-visible ring for interactive elements */
|
|
294
318
|
button:focus-visible,
|
|
295
319
|
a:focus-visible,
|
|
@@ -325,7 +349,7 @@ a:focus-visible,
|
|
|
325
349
|
/* Selection */
|
|
326
350
|
.wysiwyg-editor ::selection {
|
|
327
351
|
background: var(--amber);
|
|
328
|
-
color:
|
|
352
|
+
color: var(--amber-foreground);
|
|
329
353
|
opacity: 0.35;
|
|
330
354
|
}
|
|
331
355
|
|
package/app/app/login/page.tsx
CHANGED
|
@@ -90,7 +90,7 @@ function LoginForm() {
|
|
|
90
90
|
autoFocus
|
|
91
91
|
autoComplete="current-password"
|
|
92
92
|
required
|
|
93
|
-
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
|
|
93
|
+
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
|
94
94
|
/>
|
|
95
95
|
</div>
|
|
96
96
|
|
|
@@ -101,7 +101,7 @@ function LoginForm() {
|
|
|
101
101
|
<button
|
|
102
102
|
type="submit"
|
|
103
103
|
disabled={loading || !password}
|
|
104
|
-
className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2 bg-[var(--amber)] text-[
|
|
104
|
+
className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2 bg-[var(--amber)] text-[var(--amber-foreground)]"
|
|
105
105
|
>
|
|
106
106
|
{loading ? (
|
|
107
107
|
<Loader2 size={14} className="animate-spin" />
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useTransition, useCallback, useEffect, useRef, useSyncExternalStore, useMemo, Suspense } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft } from 'lucide-react';
|
|
5
|
+
import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft, Share2, FileText, Code } from 'lucide-react';
|
|
6
6
|
import { lazy } from 'react';
|
|
7
7
|
import MarkdownView from '@/components/MarkdownView';
|
|
8
8
|
import JsonView from '@/components/JsonView';
|
|
@@ -118,6 +118,11 @@ export default function ViewPageClient({
|
|
|
118
118
|
setSaveError('Please enter a file name');
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
|
+
// Reject path traversal and illegal filename characters
|
|
122
|
+
if (/[/\\:*?"<>|]/.test(trimmed) || trimmed.includes('..')) {
|
|
123
|
+
setSaveError('File name contains invalid characters');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
121
126
|
if (!createDraftAction) {
|
|
122
127
|
setSaveError('Draft save is not available');
|
|
123
128
|
return;
|
|
@@ -213,13 +218,13 @@ export default function ViewPageClient({
|
|
|
213
218
|
|
|
214
219
|
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
|
|
215
220
|
{saveSuccess && (
|
|
216
|
-
<span className="text-xs flex items-center gap-1.5 font-display" style={{ color: '
|
|
217
|
-
<span className="w-1.5 h-1.5 rounded-full" style={{ background: '
|
|
221
|
+
<span className="text-xs flex items-center gap-1.5 font-display" style={{ color: 'var(--success)' }}>
|
|
222
|
+
<span className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--success)' }} />
|
|
218
223
|
<span className="hidden sm:inline">saved</span>
|
|
219
224
|
</span>
|
|
220
225
|
)}
|
|
221
226
|
{saveError && (
|
|
222
|
-
<span className="text-xs text-
|
|
227
|
+
<span className="text-xs text-error hidden sm:inline">{saveError}</span>
|
|
223
228
|
)}
|
|
224
229
|
|
|
225
230
|
{/* Graph toggle — only for md files, hidden when graph plugin is disabled */}
|
|
@@ -233,8 +238,8 @@ export default function ViewPageClient({
|
|
|
233
238
|
}}
|
|
234
239
|
title={effectiveGraphMode ? 'Switch to document view' : 'Switch to Wiki Graph'}
|
|
235
240
|
>
|
|
236
|
-
<
|
|
237
|
-
<span className="hidden sm:inline">Graph</span>
|
|
241
|
+
{effectiveGraphMode ? <FileText size={13} /> : <Share2 size={13} />}
|
|
242
|
+
<span className="hidden sm:inline">{effectiveGraphMode ? 'Doc' : 'Graph'}</span>
|
|
238
243
|
</button>
|
|
239
244
|
)}
|
|
240
245
|
|
|
@@ -244,12 +249,12 @@ export default function ViewPageClient({
|
|
|
244
249
|
onClick={handleToggleRaw}
|
|
245
250
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
|
|
246
251
|
style={{
|
|
247
|
-
background: effectiveUseRaw ? 'var(--
|
|
248
|
-
color: effectiveUseRaw ? 'var(--
|
|
252
|
+
background: effectiveUseRaw ? `${'var(--amber)'}22` : 'var(--muted)',
|
|
253
|
+
color: effectiveUseRaw ? 'var(--amber)' : 'var(--muted-foreground)',
|
|
249
254
|
}}
|
|
250
255
|
title={effectiveUseRaw ? `Switch to ${registryRenderer?.name}` : 'View raw'}
|
|
251
256
|
>
|
|
252
|
-
<LayoutTemplate size={13} />
|
|
257
|
+
{effectiveUseRaw ? <LayoutTemplate size={13} /> : <Code size={13} />}
|
|
253
258
|
<span className="hidden sm:inline">{effectiveUseRaw ? registryRenderer.name : 'Raw'}</span>
|
|
254
259
|
</button>
|
|
255
260
|
)}
|
|
@@ -283,7 +288,7 @@ export default function ViewPageClient({
|
|
|
283
288
|
onClick={isDraft && showSaveAs ? handleConfirmDraftSave : handleSave}
|
|
284
289
|
disabled={isPending}
|
|
285
290
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium disabled:opacity-50 font-display"
|
|
286
|
-
style={{ background: 'var(--amber)', color: '
|
|
291
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
287
292
|
>
|
|
288
293
|
{isPending ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
|
|
289
294
|
<span className="hidden sm:inline">Save</span>
|
|
@@ -69,7 +69,7 @@ export default function ViewNotFound() {
|
|
|
69
69
|
onClick={handleCreate}
|
|
70
70
|
disabled={creating}
|
|
71
71
|
className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
72
|
-
style={{ background: 'var(--amber)', color: '
|
|
72
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
73
73
|
>
|
|
74
74
|
<FilePlus size={14} />
|
|
75
75
|
{creating
|
|
@@ -289,7 +289,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
289
289
|
{/* Attached file chips */}
|
|
290
290
|
{attachedFiles.length > 0 && (
|
|
291
291
|
<div className="px-4 pt-2.5 pb-1">
|
|
292
|
-
<div className="text-
|
|
292
|
+
<div className="text-xs text-muted-foreground/70 mb-1.5">Knowledge Base Context</div>
|
|
293
293
|
<div className="flex flex-wrap gap-1.5">
|
|
294
294
|
{attachedFiles.map(f => (
|
|
295
295
|
<FileChip key={f} path={f} onRemove={() => setAttachedFiles(prev => prev.filter(x => x !== f))} />
|
|
@@ -300,7 +300,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
300
300
|
|
|
301
301
|
{upload.localAttachments.length > 0 && (
|
|
302
302
|
<div className="px-4 pb-1">
|
|
303
|
-
<div className="text-
|
|
303
|
+
<div className="text-xs text-muted-foreground/70 mb-1.5">Uploaded Files</div>
|
|
304
304
|
<div className="flex flex-wrap gap-1.5">
|
|
305
305
|
{upload.localAttachments.map((f, idx) => (
|
|
306
306
|
<FileChip key={`${f.name}-${idx}`} path={f.name} variant="upload" onRemove={() => upload.removeAttachment(idx)} />
|
|
@@ -310,7 +310,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
310
310
|
)}
|
|
311
311
|
|
|
312
312
|
{upload.uploadError && (
|
|
313
|
-
<div className="px-4 pb-1 text-xs text-
|
|
313
|
+
<div className="px-4 pb-1 text-xs text-error">{upload.uploadError}</div>
|
|
314
314
|
)}
|
|
315
315
|
|
|
316
316
|
{/* @-mention dropdown */}
|
|
@@ -371,7 +371,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
371
371
|
<StopCircle size={15} />
|
|
372
372
|
</button>
|
|
373
373
|
) : (
|
|
374
|
-
<button type="submit" disabled={!input.trim()} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0" style={{ background: 'var(--amber)', color: '
|
|
374
|
+
<button type="submit" disabled={!input.trim()} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0" style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
375
375
|
<Send size={14} />
|
|
376
376
|
</button>
|
|
377
377
|
)}
|
|
@@ -384,7 +384,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
384
384
|
<span><kbd className="font-mono">@</kbd> {t.ask.attachFile}</span>
|
|
385
385
|
<span className="inline-flex items-center gap-1">
|
|
386
386
|
<span>Agent steps</span>
|
|
387
|
-
<select value={maxSteps} onChange={(e) => setMaxSteps(Number(e.target.value))} disabled={isLoading} className="bg-transparent border border-border rounded px-1.5 py-0.5 text-
|
|
387
|
+
<select value={maxSteps} onChange={(e) => setMaxSteps(Number(e.target.value))} disabled={isLoading} className="bg-transparent border border-border rounded px-1.5 py-0.5 text-xs text-foreground">
|
|
388
388
|
<option value={10}>10</option>
|
|
389
389
|
<option value={20}>20</option>
|
|
390
390
|
<option value={30}>30</option>
|
|
@@ -5,8 +5,8 @@ import { ChevronRight, Home, FileText, Table, Folder } from 'lucide-react';
|
|
|
5
5
|
|
|
6
6
|
function FileTypeIcon({ name }: { name: string }) {
|
|
7
7
|
const ext = name.includes('.') ? name.slice(name.lastIndexOf('.')).toLowerCase() : '';
|
|
8
|
-
if (ext === '.csv') return <Table size={13} className="text-
|
|
9
|
-
if (ext) return <FileText size={13} className="text-
|
|
8
|
+
if (ext === '.csv') return <Table size={13} className="text-success shrink-0" />;
|
|
9
|
+
if (ext) return <FileText size={13} className="text-muted-foreground shrink-0" />;
|
|
10
10
|
return <Folder size={13} className="text-yellow-400 shrink-0" />;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -15,14 +15,14 @@ interface DirViewProps {
|
|
|
15
15
|
|
|
16
16
|
function FileIcon({ node }: { node: FileNode }) {
|
|
17
17
|
if (node.type === 'directory') return <Folder size={16} className="text-yellow-400 shrink-0" />;
|
|
18
|
-
if (node.extension === '.csv') return <Table size={16} className="text-
|
|
19
|
-
return <FileText size={16} className="text-
|
|
18
|
+
if (node.extension === '.csv') return <Table size={16} className="text-success shrink-0" />;
|
|
19
|
+
return <FileText size={16} className="text-muted-foreground shrink-0" />;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function FileIconLarge({ node }: { node: FileNode }) {
|
|
23
23
|
if (node.type === 'directory') return <FolderOpen size={28} className="text-yellow-400" />;
|
|
24
|
-
if (node.extension === '.csv') return <Table size={28} className="text-
|
|
25
|
-
return <FileText size={28} className="text-
|
|
24
|
+
if (node.extension === '.csv') return <Table size={28} className="text-success" />;
|
|
25
|
+
return <FileText size={28} className="text-muted-foreground" />;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function countFiles(node: FileNode): number {
|
|
@@ -125,10 +125,10 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
|
125
125
|
{entry.name}
|
|
126
126
|
</span>
|
|
127
127
|
{entry.type === 'directory' && (
|
|
128
|
-
<span className="text-
|
|
128
|
+
<span className="text-2xs text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
|
|
129
129
|
)}
|
|
130
130
|
{entry.type === 'file' && entry.mtime && (
|
|
131
|
-
<span className="text-
|
|
131
|
+
<span className="text-2xs text-muted-foreground font-display" suppressHydrationWarning>
|
|
132
132
|
{formatTime(entry.mtime)}
|
|
133
133
|
</span>
|
|
134
134
|
)}
|
|
@@ -16,8 +16,8 @@ interface FileTreeProps {
|
|
|
16
16
|
|
|
17
17
|
function getIcon(node: FileNode) {
|
|
18
18
|
if (node.type === 'directory') return null;
|
|
19
|
-
if (node.extension === '.csv') return <Table size={14} className="text-
|
|
20
|
-
return <FileText size={14} className="text-
|
|
19
|
+
if (node.extension === '.csv') return <Table size={14} className="text-success shrink-0" />;
|
|
20
|
+
return <FileText size={14} className="text-muted-foreground shrink-0" />;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function getCurrentFilePath(pathname: string): string {
|
|
@@ -65,7 +65,7 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
|
|
|
65
65
|
className="
|
|
66
66
|
flex-1 bg-muted border border-border rounded px-2 py-1
|
|
67
67
|
text-xs text-foreground placeholder:text-muted-foreground
|
|
68
|
-
focus:outline-none focus:
|
|
68
|
+
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
|
69
69
|
"
|
|
70
70
|
/>
|
|
71
71
|
{isPending
|
|
@@ -80,7 +80,7 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
|
|
|
80
80
|
)
|
|
81
81
|
}
|
|
82
82
|
</div>
|
|
83
|
-
{error && <p className="text-xs text-
|
|
83
|
+
{error && <p className="text-xs text-error mt-0.5 px-1">{error}</p>}
|
|
84
84
|
</div>
|
|
85
85
|
);
|
|
86
86
|
}
|
|
@@ -161,7 +161,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate }: {
|
|
|
161
161
|
if (e.key === 'Escape') setRenaming(false);
|
|
162
162
|
}}
|
|
163
163
|
onBlur={commitRename}
|
|
164
|
-
className="w-full bg-muted border border-
|
|
164
|
+
className="w-full bg-muted border border-border rounded px-2 py-0.5 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
165
165
|
/>
|
|
166
166
|
{isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
|
|
167
167
|
</div>
|
|
@@ -304,7 +304,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
304
304
|
if (e.key === 'Escape') setRenaming(false);
|
|
305
305
|
}}
|
|
306
306
|
onBlur={commitRename}
|
|
307
|
-
className="w-full bg-muted border border-
|
|
307
|
+
className="w-full bg-muted border border-border rounded px-2 py-0.5 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
308
308
|
/>
|
|
309
309
|
{isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
|
|
310
310
|
</div>
|
|
@@ -334,7 +334,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
334
334
|
<button onClick={startRename} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.rename}>
|
|
335
335
|
<Pencil size={12} />
|
|
336
336
|
</button>
|
|
337
|
-
<button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-
|
|
337
|
+
<button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-error hover:bg-muted transition-colors" title={t.fileTree.delete}>
|
|
338
338
|
<Trash2 size={12} />
|
|
339
339
|
</button>
|
|
340
340
|
</div>
|
|
@@ -102,7 +102,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
102
102
|
{suggestions[suggestionIdx]}
|
|
103
103
|
</span>
|
|
104
104
|
<kbd
|
|
105
|
-
className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-
|
|
105
|
+
className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-xs font-mono font-medium"
|
|
106
106
|
style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}
|
|
107
107
|
>
|
|
108
108
|
⌘/
|
|
@@ -118,7 +118,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
118
118
|
>
|
|
119
119
|
<Search size={14} />
|
|
120
120
|
<span className="hidden sm:inline">{t.home.shortcuts.searchFiles}</span>
|
|
121
|
-
<kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-
|
|
121
|
+
<kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-2xs font-mono" style={{ background: 'var(--muted)' }}>
|
|
122
122
|
⌘K
|
|
123
123
|
</kbd>
|
|
124
124
|
</button>
|
|
@@ -189,17 +189,17 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
189
189
|
{r.name}
|
|
190
190
|
</span>
|
|
191
191
|
</div>
|
|
192
|
-
<p className="text-
|
|
192
|
+
<p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
|
|
193
193
|
{r.description}
|
|
194
194
|
</p>
|
|
195
195
|
{hintId === r.id ? (
|
|
196
|
-
<p className="text-
|
|
196
|
+
<p className="text-2xs animate-in" style={{ color: 'var(--amber)' }} role="status">
|
|
197
197
|
{(t.home.createToActivate ?? 'Create {file} to activate').replace('{file}', entryPath ?? '')}
|
|
198
198
|
</p>
|
|
199
199
|
) : (
|
|
200
200
|
<div className="flex flex-wrap gap-1">
|
|
201
201
|
{r.tags.slice(0, 3).map(tag => (
|
|
202
|
-
<span key={tag} className="text-
|
|
202
|
+
<span key={tag} className="text-2xs px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
203
203
|
{tag}
|
|
204
204
|
</span>
|
|
205
205
|
))}
|
|
@@ -222,12 +222,12 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
222
222
|
{r.name}
|
|
223
223
|
</span>
|
|
224
224
|
</div>
|
|
225
|
-
<p className="text-
|
|
225
|
+
<p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
|
|
226
226
|
{r.description}
|
|
227
227
|
</p>
|
|
228
228
|
<div className="flex flex-wrap gap-1">
|
|
229
229
|
{r.tags.slice(0, 3).map(tag => (
|
|
230
|
-
<span key={tag} className="text-
|
|
230
|
+
<span key={tag} className="text-2xs px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
231
231
|
{tag}
|
|
232
232
|
</span>
|
|
233
233
|
))}
|
|
@@ -278,7 +278,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
278
278
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-100 group-hover:translate-x-0.5 hover:bg-muted"
|
|
279
279
|
>
|
|
280
280
|
{isCSV
|
|
281
|
-
? <Table size={13} className="shrink-0" style={{ color: '
|
|
281
|
+
? <Table size={13} className="shrink-0" style={{ color: 'var(--success)' }} />
|
|
282
282
|
: <FileText size={13} className="shrink-0" style={{ color: 'var(--muted-foreground)' }} />
|
|
283
283
|
}
|
|
284
284
|
<div className="flex-1 min-w-0">
|
|
@@ -110,7 +110,7 @@ export default function OnboardingView() {
|
|
|
110
110
|
|
|
111
111
|
{/* Directory preview */}
|
|
112
112
|
<div
|
|
113
|
-
className="w-full rounded-lg px-3 py-2 text-
|
|
113
|
+
className="w-full rounded-lg px-3 py-2 text-xs leading-relaxed font-display"
|
|
114
114
|
style={{
|
|
115
115
|
background: 'var(--muted)',
|
|
116
116
|
color: 'var(--muted-foreground)',
|
|
@@ -158,7 +158,7 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
|
|
|
158
158
|
`}
|
|
159
159
|
>
|
|
160
160
|
{ext === '.csv'
|
|
161
|
-
? <Table size={14} className="text-
|
|
161
|
+
? <Table size={14} className="text-success shrink-0 mt-0.5" />
|
|
162
162
|
: <FileText size={14} className="text-muted-foreground shrink-0 mt-0.5" />
|
|
163
163
|
}
|
|
164
164
|
<div className="min-w-0 flex-1">
|
|
@@ -234,7 +234,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
234
234
|
)}
|
|
235
235
|
<div className="flex items-center gap-1.5 text-xs">
|
|
236
236
|
{status === 'saved' && (
|
|
237
|
-
<><CheckCircle2 size={13} className="text-
|
|
237
|
+
<><CheckCircle2 size={13} className="text-success" /><span className="text-success">{t.settings.saved}</span></>
|
|
238
238
|
)}
|
|
239
239
|
{status === 'error' && (
|
|
240
240
|
<><AlertCircle size={13} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>
|
|
@@ -245,7 +245,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
245
245
|
onClick={handleSave}
|
|
246
246
|
disabled={saving || !data}
|
|
247
247
|
className="flex items-center gap-1.5 px-4 py-1.5 text-sm rounded-lg disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
|
|
248
|
-
style={{ background: 'var(--amber)', color: '
|
|
248
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
249
249
|
>
|
|
250
250
|
{saving ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
|
|
251
251
|
{t.settings.save}
|