@geminilight/mindos 0.5.62 → 0.5.64

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 (41) hide show
  1. package/app/app/api/changes/route.ts +7 -1
  2. package/app/app/api/mcp/install-skill/route.ts +9 -24
  3. package/app/app/api/mcp/status/route.ts +1 -1
  4. package/app/app/layout.tsx +1 -0
  5. package/app/app/page.tsx +1 -2
  6. package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
  7. package/app/components/HomeContent.tsx +41 -6
  8. package/app/components/RightAgentDetailPanel.tsx +1 -0
  9. package/app/components/SidebarLayout.tsx +1 -0
  10. package/app/components/agents/AgentsContentPage.tsx +20 -16
  11. package/app/components/agents/AgentsMcpSection.tsx +178 -65
  12. package/app/components/agents/AgentsOverviewSection.tsx +1 -1
  13. package/app/components/agents/AgentsSkillsSection.tsx +78 -55
  14. package/app/components/agents/agents-content-model.ts +16 -0
  15. package/app/components/changes/ChangesBanner.tsx +90 -13
  16. package/app/components/changes/ChangesContentPage.tsx +134 -51
  17. package/app/components/panels/AgentsPanel.tsx +14 -28
  18. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -4
  19. package/app/components/panels/AgentsPanelAgentGroups.tsx +5 -6
  20. package/app/components/panels/AgentsPanelAgentListRow.tsx +30 -5
  21. package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
  22. package/app/components/panels/PluginsPanel.tsx +3 -3
  23. package/app/components/renderers/agent-inspector/manifest.ts +2 -0
  24. package/app/components/renderers/config/manifest.ts +1 -0
  25. package/app/components/renderers/csv/manifest.ts +1 -0
  26. package/app/components/settings/PluginsTab.tsx +4 -3
  27. package/app/hooks/useMcpData.tsx +3 -2
  28. package/app/lib/core/content-changes.ts +148 -8
  29. package/app/lib/fs.ts +7 -1
  30. package/app/lib/i18n-en.ts +58 -3
  31. package/app/lib/i18n-zh.ts +58 -3
  32. package/app/lib/mcp-agents.ts +42 -0
  33. package/app/lib/renderers/registry.ts +10 -0
  34. package/app/next-env.d.ts +1 -1
  35. package/bin/lib/mcp-agents.js +38 -13
  36. package/package.json +1 -1
  37. package/scripts/migrate-agent-diff.js +146 -0
  38. package/scripts/setup.js +12 -17
  39. package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
  40. package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
  41. package/app/components/renderers/diff/manifest.ts +0 -14
@@ -0,0 +1,178 @@
1
+ ---
2
+ name: plugin-core-builtin-migration
3
+ description: >
4
+ 将 MindOS 渲染器插件从“可选插件”升级为“完全内置能力(core builtin)”的通用流程。
5
+ 当用户要求“把某插件改成内置/核心”“插件不应可关闭”“插件主程序化”或需要将旧入口迁移到
6
+ 新主流程并保留兼容迁移能力时触发。
7
+ ---
8
+
9
+ # Plugin Core Built-in Migration
10
+
11
+ 把单个插件改造成“稳定、默认、不可缺席”的产品能力,并沉淀可复用迁移步骤。
12
+
13
+ 适用场景:
14
+ - 某插件已经成为核心体验(例如 TODO、CSV、配置、变更中心)
15
+ - 旧插件入口要退役,但历史数据要可迁移
16
+ - 需要避免“某页面漏 import 导致插件空列表”这类注册漂移
17
+
18
+ ---
19
+
20
+ ## 目标定义(先对齐)
21
+
22
+ 在本项目中,“完全内置”建议同时满足:
23
+
24
+ 1. **注册稳定**:渲染器注册有且仅有一个**客户端根入口**(推荐 `SidebarLayout` / `ShellLayout`)
25
+ 2. **默认可用**:`manifest.builtin = true`
26
+ 3. **不可禁用**(如需真正 core):`manifest.core = true`(`registry` 会强制启用)
27
+ 4. **可观测可维护**:插件页、设置页、文档、测试一致
28
+ 5. **历史兼容**:旧入口数据有迁移方案,不丢内容
29
+ 6. **表层语义清晰**:区分“插件(可管理)”和“应用内建能力(不在插件面板展示)”
30
+
31
+ ---
32
+
33
+ ## 质量闸门(必走)
34
+
35
+ 每次执行本 Skill 时,默认同时应用以下质量视角(按顺序):
36
+
37
+ 1. **产品设计视角**(`product-designer`)
38
+ - 检查信息层级、任务路径、主次操作、反馈时机
39
+ - 明确“主路径 3 步内可完成”,避免迁移后流程变长
40
+
41
+ 2. **UI/UX 视角**(`ui-design-patterns`)
42
+ - 检查按钮层级、交互一致性、空状态/错误状态/加载状态
43
+ - 禁止无意义下划线链接滥用;主 CTA 与次级 CTA 视觉层级明确
44
+ - 深色模式可读性必须通过(按钮文字对比、tag 对比、focus 可见)
45
+
46
+ 3. **实现质量视角**
47
+ - 迁移逻辑幂等(重复执行不重复导入)
48
+ - 注册机制单点化(禁止多处散落 import 造成漂移)
49
+ - 避免仅服务端注册导致客户端 `0/0` 空列表
50
+ - 兼容代码失败不阻断主流程(best-effort + 可观测)
51
+
52
+ 4. **验证视角**
53
+ - 最少通过:core/API 回归测试 + lint
54
+ - UI 变更至少进行一次人工检查(亮色/暗色)
55
+
56
+ 若任一步不满足,迁移不得判定完成。
57
+
58
+ ---
59
+
60
+ ## 执行流程(通用)
61
+
62
+ ### Step 1) 盘点现状与边界
63
+
64
+ 先确认插件的四类事实来源:
65
+
66
+ - 代码入口:`app/components/renderers/<plugin>/manifest.ts`
67
+ - 注册入口:`app/lib/renderers/index.ts`(自动生成)
68
+ - 使用入口:`resolveRenderer()` 调用链(如 `ViewPageClient`)
69
+ - 展示入口:插件面板/设置页(优先使用 `getPluginRenderers()`)
70
+
71
+ 并回答:
72
+ - 该插件是否应 **core**(不可关闭)?
73
+ - 是否存在旧文件协议/旧入口(如 `Agent-Diff.md`)需要迁移?
74
+
75
+ ### Step 2) 升级插件声明
76
+
77
+ 在 manifest 中设置:
78
+
79
+ - `builtin: true`(内置)
80
+ - `core: true`(若要完全内置且不可关闭)
81
+ - `appBuiltinFeature: true`(若是“应用内建能力”且不应出现在插件管理面板)
82
+ - `match` 保持明确,避免误匹配
83
+
84
+ 如果插件要被替换/下线:
85
+ - 删除旧 renderer 文件
86
+ - 重新生成 `app/lib/renderers/index.ts`
87
+
88
+ ### Step 3) 统一注册机制(防漂移)
89
+
90
+ 必须收敛为单点初始化(客户端):
91
+
92
+ - 在客户端根组件(推荐 `SidebarLayout`)引入 `@/lib/renderers/index`
93
+ - 移除其他页面/组件分散 import(避免重复与遗漏并存)
94
+
95
+ 验收标准:
96
+ - 任意路由进入插件页,插件列表不应出现 `0/0` 的假空状态
97
+ - 在“应用内建能力”模式下,被标记能力应从插件面板消失但渲染仍正常
98
+
99
+ ### Step 4) 历史兼容迁移(关键)
100
+
101
+ 如果旧插件依赖旧文件格式:
102
+
103
+ 1. 在核心数据层实现“懒迁移”:
104
+ - 首次读取时自动导入旧格式
105
+ - 写入新格式(结构化 JSON)
106
+ 2. 迁移后清理旧入口文件(防止双写和重复导入)
107
+ 3. 提供一次性脚本(离线/手动修复):
108
+ - `scripts/migrate-*.js`
109
+
110
+ ### Step 5) UI/UX 对齐
111
+
112
+ - 插件页:状态、可开关策略与 core 语义一致
113
+ - 设置页:显示 builtin/core 标签逻辑一致
114
+ - 若设为 `appBuiltinFeature`:在插件页/设置页/首页 Extensions 一致隐藏
115
+ - 新主流程页面补齐筛选、空状态、加载、已读等基本交互
116
+
117
+ ### Step 6) 文档与知识同步
118
+
119
+ 至少同步:
120
+
121
+ - `wiki/plugins/README.md`
122
+ - `wiki/60-stage-plugins.md`
123
+ - 相关 spec / pitfalls / refs(若行为模型发生变化)
124
+
125
+ 并记录迁移决策:
126
+ - 为什么从插件入口转主程序能力
127
+ - 旧入口如何兼容、何时清理
128
+
129
+ ### Step 7) 测试与回归
130
+
131
+ 最低测试集合:
132
+
133
+ - core 层迁移测试(旧格式 -> 新格式)
134
+ - API 层筛选/查询测试(若有 changes/feed 类接口)
135
+ - UI 基础 smoke(插件列表非空、核心按钮可用)
136
+ - 分类测试(`appBuiltinFeature` 能否正确从插件表层隐藏)
137
+
138
+ 推荐命令:
139
+
140
+ ```bash
141
+ npm --prefix app run test -- __tests__/core/content-changes.test.ts __tests__/api/changes.test.ts
142
+ ```
143
+
144
+ ---
145
+
146
+ ## AgentDiff -> Built-in 的参考映射
147
+
148
+ 这次迁移可作为样板:
149
+
150
+ - 旧:`Agent-Diff.md` + ```agent-diff``` renderer
151
+ - 新:`.mindos/change-log.json` + `/api/changes` + `/changes` 主页面 + 全局提醒
152
+ - 兼容:读取旧 block 自动导入;导入后删除 `Agent-Diff.md`
153
+ - 治理:删除 `diff-viewer` renderer 与文档旧入口,保留结构化审计链路
154
+
155
+ ---
156
+
157
+ ## 常见坑位清单
158
+
159
+ 1. **只删插件没迁移旧数据** -> 用户历史丢失
160
+ 2. **注册入口分散/仅服务端注册** -> 客户端插件面板出现 `0/0`
161
+ 3. **文档未同步** -> 用户看到过时入口
162
+ 4. **core/builtin/appBuiltinFeature 语义混淆** -> 本应“应用内建”的能力仍出现在插件面板
163
+ 5. **迁移不幂等** -> 每次读取重复导入同一批旧数据
164
+
165
+ ---
166
+
167
+ ## 交付 Checklist(可复制)
168
+
169
+ - [ ] manifest 层完成 builtin/core 定义
170
+ - [ ] 如需“非插件化”,manifest 增加 `appBuiltinFeature: true`
171
+ - [ ] 全局注册单点化(客户端根入口统一引入)
172
+ - [ ] 旧入口迁移逻辑已实现且幂等
173
+ - [ ] 迁移后旧文件可清理
174
+ - [ ] 提供一次性迁移脚本
175
+ - [ ] 插件页/设置页状态正确(含 appBuiltinFeature 隐藏策略)
176
+ - [ ] 文档已同步更新
177
+ - [ ] 测试通过
178
+
@@ -1,311 +0,0 @@
1
- 'use client';
2
-
3
- import { useMemo, useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import { GitCompare, CheckCircle2, XCircle, FileEdit, ChevronDown } from 'lucide-react';
6
- import { apiFetch } from '@/lib/api';
7
- import type { RendererContext } from '@/lib/renderers/registry';
8
-
9
- // ─── Diff entry format ────────────────────────────────────────────────────────
10
- // Agent writes diff entries as fenced blocks:
11
- //
12
- // ```agent-diff
13
- // { "ts": "2025-01-15T10:30:00Z", "path": "Profile/Identity.md",
14
- // "tool": "mindos_write_file",
15
- // "before": "...full old content...",
16
- // "after": "...full new content..." }
17
- // ```
18
-
19
- interface DiffEntry {
20
- ts: string;
21
- path: string;
22
- tool: string;
23
- before: string;
24
- after: string;
25
- }
26
-
27
- // ─── Parser ───────────────────────────────────────────────────────────────────
28
-
29
- function parseDiffs(content: string): DiffEntry[] {
30
- const entries: DiffEntry[] = [];
31
- const re = /```agent-diff\n([\s\S]*?)```/g;
32
- let m: RegExpExecArray | null;
33
- while ((m = re.exec(content)) !== null) {
34
- try {
35
- const entry = JSON.parse(m[1].trim()) as DiffEntry;
36
- if (entry.path && entry.ts) entries.push(entry);
37
- } catch { /* skip */ }
38
- }
39
- return entries.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
40
- }
41
-
42
- // ─── Diff algorithm (line-level Myers-light) ──────────────────────────────────
43
-
44
- type LineChange = { type: 'equal' | 'insert' | 'delete'; text: string };
45
-
46
- function diffLines(oldText: string, newText: string): LineChange[] {
47
- const oldLines = oldText.split('\n');
48
- const newLines = newText.split('\n');
49
-
50
- // LCS-based diff (simple patience-like for short files)
51
- const result: LineChange[] = [];
52
-
53
- // Build LCS table
54
- const m = oldLines.length, n = newLines.length;
55
- // For large files, limit context
56
- if (m > 500 || n > 500) {
57
- // Truncate and just show a summary
58
- const added = newLines.filter(l => !oldLines.includes(l)).length;
59
- const removed = oldLines.filter(l => !newLines.includes(l)).length;
60
- result.push({ type: 'delete', text: `[... ${removed} lines removed, ${added} lines added — file too large for line diff ...]` });
61
- return result;
62
- }
63
-
64
- const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
65
- for (let i = m - 1; i >= 0; i--) {
66
- for (let j = n - 1; j >= 0; j--) {
67
- if (oldLines[i] === newLines[j]) {
68
- dp[i][j] = 1 + dp[i + 1][j + 1];
69
- } else {
70
- dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
71
- }
72
- }
73
- }
74
-
75
- let i = 0, j = 0;
76
- while (i < m || j < n) {
77
- if (i < m && j < n && oldLines[i] === newLines[j]) {
78
- result.push({ type: 'equal', text: oldLines[i] });
79
- i++; j++;
80
- } else if (j < n && (i >= m || dp[i][j + 1] >= dp[i + 1][j])) {
81
- result.push({ type: 'insert', text: newLines[j] });
82
- j++;
83
- } else {
84
- result.push({ type: 'delete', text: oldLines[i] });
85
- i++;
86
- }
87
- }
88
-
89
- return result;
90
- }
91
-
92
- // Collapse long equal runs — show 3 context lines around changes
93
- function collapseContext(changes: LineChange[], ctx = 3): Array<LineChange | { type: 'collapse'; count: number }> {
94
- type AnyLine = LineChange | { type: 'collapse'; count: number };
95
- const result: AnyLine[] = [];
96
- const changed = new Set<number>();
97
-
98
- changes.forEach((c, i) => { if (c.type !== 'equal') { for (let k = Math.max(0, i - ctx); k <= Math.min(changes.length - 1, i + ctx); k++) changed.add(k); } });
99
-
100
- let skipStart = -1;
101
- for (let i = 0; i < changes.length; i++) {
102
- if (changed.has(i)) {
103
- if (skipStart !== -1) {
104
- result.push({ type: 'collapse', count: i - skipStart });
105
- skipStart = -1;
106
- }
107
- result.push(changes[i]);
108
- } else {
109
- if (skipStart === -1) skipStart = i;
110
- }
111
- }
112
- if (skipStart !== -1) result.push({ type: 'collapse', count: changes.length - skipStart });
113
-
114
- return result;
115
- }
116
-
117
- // ─── Helpers ──────────────────────────────────────────────────────────────────
118
-
119
- function relativeTs(ts: string): string {
120
- const diff = Date.now() - new Date(ts).getTime();
121
- const m = Math.floor(diff / 60000);
122
- const h = Math.floor(diff / 3600000);
123
- const d = Math.floor(diff / 86400000);
124
- if (m < 1) return 'just now';
125
- if (m < 60) return `${m}m ago`;
126
- if (h < 24) return `${h}h ago`;
127
- return `${d}d ago`;
128
- }
129
-
130
- function stats(changes: LineChange[]): { added: number; removed: number } {
131
- return {
132
- added: changes.filter(c => c.type === 'insert').length,
133
- removed: changes.filter(c => c.type === 'delete').length,
134
- };
135
- }
136
-
137
- // ─── Diff card ────────────────────────────────────────────────────────────────
138
-
139
- function DiffCard({ entry, saveAction, fullContent }: {
140
- entry: DiffEntry;
141
- saveAction: (c: string) => Promise<void>;
142
- fullContent: string;
143
- }) {
144
- const router = useRouter();
145
- const [expanded, setExpanded] = useState(false);
146
- const [approved, setApproved] = useState<boolean | null>(null);
147
-
148
- const changes = useMemo(() => diffLines(entry.before, entry.after), [entry]);
149
- const collapsed = useMemo(() => collapseContext(changes), [changes]);
150
- const { added, removed } = stats(changes);
151
-
152
- const toolShort = entry.tool.replace('mindos_', '');
153
-
154
- async function handleApprove() {
155
- setApproved(true);
156
- // Mark this diff as approved by updating the block in the source file
157
- const updated = fullContent.replace(
158
- `"ts": "${entry.ts}", "path": "${entry.path}"`,
159
- `"ts": "${entry.ts}", "path": "${entry.path}", "approved": true`,
160
- );
161
- await saveAction(updated);
162
- }
163
-
164
- async function handleReject() {
165
- setApproved(false);
166
- // Revert: write the "before" content back to the target file
167
- await apiFetch('/api/file', {
168
- method: 'POST',
169
- headers: { 'Content-Type': 'application/json' },
170
- body: JSON.stringify({ op: 'save_file', path: entry.path, content: entry.before }),
171
- });
172
- const updated = fullContent.replace(
173
- `"ts": "${entry.ts}", "path": "${entry.path}"`,
174
- `"ts": "${entry.ts}", "path": "${entry.path}", "approved": false, "reverted": true`,
175
- );
176
- await saveAction(updated);
177
- }
178
-
179
- return (
180
- <div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden', background: 'var(--card)', marginBottom: 10 }}>
181
- {/* header */}
182
- <div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '10px 14px', borderBottom: expanded ? '1px solid var(--border)' : 'none' }}>
183
- <FileEdit size={13} style={{ color: 'var(--amber)', flexShrink: 0 }} />
184
- <span
185
- className="font-display"
186
- style={{ fontSize: '0.78rem', color: 'var(--amber)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }}
187
- onClick={() => router.push('/view/' + entry.path.split('/').map(encodeURIComponent).join('/'))}
188
- title={entry.path}
189
- >
190
- {entry.path}
191
- </span>
192
-
193
- {/* diff stats */}
194
- <span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--success)', flexShrink: 0 }}>+{added}</span>
195
- <span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--error)', flexShrink: 0 }}>−{removed}</span>
196
-
197
- {/* tool badge */}
198
- <span className="font-display" style={{ fontSize: '0.65rem', padding: '1px 7px', borderRadius: 999, background: 'var(--muted)', color: 'var(--muted-foreground)', flexShrink: 0 }}>
199
- {toolShort}
200
- </span>
201
-
202
- {/* timestamp */}
203
- <span className="font-display" style={{ fontSize: '0.65rem', color: 'var(--muted-foreground)', opacity: 0.6, flexShrink: 0 }}>
204
- {relativeTs(entry.ts)}
205
- </span>
206
-
207
- {/* approve/reject — only if not yet decided */}
208
- {approved === null ? (
209
- <>
210
- <button
211
- onClick={handleApprove}
212
- title="Approve this change"
213
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--success)', display: 'flex', alignItems: 'center' }}
214
- >
215
- <CheckCircle2 size={15} />
216
- </button>
217
- <button
218
- onClick={handleReject}
219
- title="Reject & revert this change"
220
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--error)', display: 'flex', alignItems: 'center' }}
221
- >
222
- <XCircle size={15} />
223
- </button>
224
- </>
225
- ) : (
226
- <span className="font-display" style={{ fontSize: '0.68rem', color: approved ? 'var(--success)' : 'var(--error)' }}>
227
- {approved ? '✓ approved' : '✕ reverted'}
228
- </span>
229
- )}
230
-
231
- <button
232
- onClick={() => setExpanded(v => !v)}
233
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--muted-foreground)', display: 'flex', alignItems: 'center' }}
234
- >
235
- <ChevronDown size={13} style={{ transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
236
- </button>
237
- </div>
238
-
239
- {/* diff view */}
240
- {expanded && (
241
- <div className="font-display" style={{ fontSize: '0.72rem', lineHeight: 1.5, overflowX: 'auto' }}>
242
- {collapsed.map((line, i) => {
243
- if (line.type === 'collapse') {
244
- return (
245
- <div key={i} style={{ padding: '2px 14px', background: 'var(--muted)', color: 'var(--muted-foreground)', opacity: 0.6, fontSize: '0.65rem' }}>
246
- ··· {line.count} unchanged lines ···
247
- </div>
248
- );
249
- }
250
- const bg =
251
- line.type === 'insert' ? 'rgba(122,173,128,0.12)' :
252
- line.type === 'delete' ? 'rgba(200,80,80,0.10)' :
253
- 'transparent';
254
- const color =
255
- line.type === 'insert' ? 'var(--success)' :
256
- line.type === 'delete' ? 'var(--error)' :
257
- 'var(--muted-foreground)';
258
- const prefix =
259
- line.type === 'insert' ? '+' :
260
- line.type === 'delete' ? '−' :
261
- ' ';
262
- return (
263
- <div key={i} style={{ display: 'flex', background: bg, borderLeft: line.type !== 'equal' ? `2px solid ${color}` : '2px solid transparent' }}>
264
- <span style={{ width: 20, textAlign: 'center', color, opacity: 0.8, flexShrink: 0, userSelect: 'none' }}>{prefix}</span>
265
- <span style={{ padding: '1px 8px 1px 0', color: line.type === 'equal' ? 'var(--muted-foreground)' : color, whiteSpace: 'pre', flex: 1 }}>
266
- {line.text || ' '}
267
- </span>
268
- </div>
269
- );
270
- })}
271
- </div>
272
- )}
273
- </div>
274
- );
275
- }
276
-
277
- // ─── Main renderer ────────────────────────────────────────────────────────────
278
-
279
- export function DiffRenderer({ content, saveAction }: RendererContext) {
280
- const entries = useMemo(() => parseDiffs(content), [content]);
281
-
282
- if (entries.length === 0) {
283
- return (
284
- <div className="font-display" style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 12 }}>
285
- <GitCompare size={28} style={{ margin: '0 auto 10px', opacity: 0.3 }} />
286
- <p>No agent diffs logged yet.</p>
287
- <p style={{ marginTop: 6, opacity: 0.6, fontSize: 11 }}>
288
- Agent writes appear here as <code style={{ background: 'var(--muted)', padding: '1px 5px', borderRadius: 4 }}>```agent-diff</code> blocks.
289
- </p>
290
- </div>
291
- );
292
- }
293
-
294
- const totalAdded = entries.reduce((acc, e) => acc + diffLines(e.before, e.after).filter(c => c.type === 'insert').length, 0);
295
- const totalRemoved = entries.reduce((acc, e) => acc + diffLines(e.before, e.after).filter(c => c.type === 'delete').length, 0);
296
-
297
- return (
298
- <div style={{ maxWidth: 800, margin: '0 auto', padding: '1.5rem 0' }}>
299
- {/* stats bar */}
300
- <div className="font-display" style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: '1.2rem', fontSize: 11, color: 'var(--muted-foreground)' }}>
301
- <span>{entries.length} change{entries.length !== 1 ? 's' : ''}</span>
302
- <span style={{ color: 'var(--success)' }}>+{totalAdded}</span>
303
- <span style={{ color: 'var(--error)' }}>−{totalRemoved}</span>
304
- </div>
305
-
306
- {entries.map((entry, i) => (
307
- <DiffCard key={i} entry={entry} saveAction={saveAction} fullContent={content} />
308
- ))}
309
- </div>
310
- );
311
- }
@@ -1,14 +0,0 @@
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
- };