@geminilight/mindos 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/app/app/api/mcp/agents/route.ts +72 -0
  2. package/app/app/api/mcp/install/route.ts +95 -0
  3. package/app/app/api/mcp/status/route.ts +47 -0
  4. package/app/app/api/setup/check-port/route.ts +41 -0
  5. package/app/app/api/skills/route.ts +208 -0
  6. package/app/app/api/sync/route.ts +54 -3
  7. package/app/app/api/update-check/route.ts +52 -0
  8. package/app/app/globals.css +12 -0
  9. package/app/app/layout.tsx +4 -2
  10. package/app/app/login/page.tsx +20 -13
  11. package/app/app/page.tsx +19 -2
  12. package/app/app/setup/page.tsx +2 -0
  13. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  14. package/app/app/view/[...path]/loading.tsx +1 -1
  15. package/app/app/view/[...path]/not-found.tsx +101 -0
  16. package/app/components/AskFab.tsx +1 -1
  17. package/app/components/AskModal.tsx +1 -1
  18. package/app/components/Backlinks.tsx +1 -1
  19. package/app/components/Breadcrumb.tsx +13 -3
  20. package/app/components/CsvView.tsx +5 -6
  21. package/app/components/DirView.tsx +42 -21
  22. package/app/components/FindInPage.tsx +211 -0
  23. package/app/components/HomeContent.tsx +97 -44
  24. package/app/components/JsonView.tsx +1 -2
  25. package/app/components/MarkdownEditor.tsx +1 -2
  26. package/app/components/OnboardingView.tsx +6 -7
  27. package/app/components/SettingsModal.tsx +5 -2
  28. package/app/components/SetupWizard.tsx +499 -172
  29. package/app/components/Sidebar.tsx +1 -1
  30. package/app/components/UpdateBanner.tsx +101 -0
  31. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  32. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  33. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  34. package/app/components/renderers/backlinks/manifest.ts +14 -0
  35. package/app/components/renderers/config/manifest.ts +14 -0
  36. package/app/components/renderers/csv/BoardView.tsx +12 -12
  37. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  38. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  39. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  40. package/app/components/renderers/csv/TableView.tsx +4 -5
  41. package/app/components/renderers/csv/manifest.ts +14 -0
  42. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  43. package/app/components/renderers/diff/manifest.ts +14 -0
  44. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  45. package/app/components/renderers/graph/manifest.ts +14 -0
  46. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  47. package/app/components/renderers/summary/manifest.ts +14 -0
  48. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  49. package/app/components/renderers/timeline/manifest.ts +14 -0
  50. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  51. package/app/components/renderers/todo/manifest.ts +14 -0
  52. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  53. package/app/components/renderers/workflow/manifest.ts +14 -0
  54. package/app/components/settings/McpTab.tsx +549 -0
  55. package/app/components/settings/SyncTab.tsx +139 -50
  56. package/app/components/settings/types.ts +1 -1
  57. package/app/data/pages/home.png +0 -0
  58. package/app/lib/i18n.ts +226 -19
  59. package/app/lib/renderers/index.ts +20 -89
  60. package/app/lib/renderers/registry.ts +4 -1
  61. package/app/lib/settings.ts +3 -0
  62. package/app/package.json +1 -0
  63. package/app/types/semver.d.ts +8 -0
  64. package/bin/cli.js +137 -24
  65. package/bin/lib/build.js +53 -18
  66. package/bin/lib/colors.js +3 -1
  67. package/bin/lib/config.js +4 -0
  68. package/bin/lib/constants.js +2 -0
  69. package/bin/lib/debug.js +10 -0
  70. package/bin/lib/mcp-install.js +4 -1
  71. package/bin/lib/port.js +8 -2
  72. package/bin/lib/startup.js +21 -20
  73. package/bin/lib/stop.js +41 -3
  74. package/bin/lib/sync.js +65 -53
  75. package/bin/lib/update-check.js +94 -0
  76. package/bin/lib/utils.js +2 -2
  77. package/package.json +1 -1
  78. package/scripts/gen-renderer-index.js +57 -0
  79. package/scripts/setup.js +205 -10
  80. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
@@ -136,9 +136,8 @@ const WikiNode = memo(function WikiNode({ data }: NodeProps) {
136
136
  <div
137
137
  onClick={handleClick}
138
138
  title={id as string}
139
- className="group"
139
+ className="group font-display"
140
140
  style={{
141
- fontFamily: "'IBM Plex Mono', monospace",
142
141
  fontSize: 10 * scale,
143
142
  padding: `${4 * scale}px ${12 * scale}px`,
144
143
  borderRadius: 999, // Pill shape
@@ -326,7 +325,7 @@ export function GraphRenderer({ filePath }: RendererContext) {
326
325
  justifyContent: 'center',
327
326
  }}
328
327
  >
329
- <span style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace", fontSize: 12 }}>
328
+ <span className="font-display" style={{ color: 'var(--muted-foreground)', fontSize: 12 }}>
330
329
  {loading ? 'Building graph…' : 'Loading…'}
331
330
  </span>
332
331
  </div>
@@ -346,8 +345,8 @@ export function GraphRenderer({ filePath }: RendererContext) {
346
345
  }}
347
346
  >
348
347
  <span
348
+ className="font-display"
349
349
  style={{
350
- fontFamily: "'IBM Plex Mono', monospace",
351
350
  fontSize: 11,
352
351
  color: 'var(--muted-foreground)',
353
352
  }}
@@ -368,11 +367,11 @@ export function GraphRenderer({ filePath }: RendererContext) {
368
367
  <button
369
368
  key={btn.id}
370
369
  onClick={() => setScope(btn.id)}
370
+ className="font-display"
371
371
  style={{
372
372
  padding: '3px 12px',
373
373
  borderRadius: 5,
374
374
  fontSize: 11,
375
- fontFamily: "'IBM Plex Mono', monospace",
376
375
  cursor: 'pointer',
377
376
  border: 'none',
378
377
  outline: 'none',
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'graph',
5
+ name: 'Wiki Graph',
6
+ description: 'Force-directed graph of wikilink references across all markdown files. Supports Global and Local (2-hop) scope filters.',
7
+ author: 'MindOS',
8
+ icon: '🕸️',
9
+ tags: ['graph', 'wiki', 'links', 'visualization'],
10
+ builtin: true,
11
+ entryPath: 'README.md',
12
+ match: ({ extension }) => extension === 'md',
13
+ load: () => import('./GraphRenderer').then(m => ({ default: m.GraphRenderer })),
14
+ };
@@ -34,7 +34,7 @@ function renderMarkdown(md: string): string {
34
34
  .replace(/^# (.+)$/gm, '<h1 style="font-size:1rem;font-weight:700;color:var(--foreground);margin:1.2em 0 .4em">$1</h1>')
35
35
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
36
36
  .replace(/\*(.+?)\*/g, '<em>$1</em>')
37
- .replace(/`(.+?)`/g, '<code style="font-family:\'IBM Plex Mono\',monospace;font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>')
37
+ .replace(/`(.+?)`/g, '<code class="font-display" style="font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>')
38
38
  .replace(/^[-*] (.+)$/gm, '<li style="margin:.2em 0;padding-left:.3em">$1</li>')
39
39
  .replace(/(<li[^>]*>.*<\/li>\n?)+/g, s => `<ul style="margin:.4em 0;padding-left:1.4em;list-style:disc">${s}</ul>`)
40
40
  .replace(/\n{2,}/g, '</p><p style="margin:.5em 0;font-size:.85rem;line-height:1.7;color:var(--foreground)">')
@@ -136,7 +136,7 @@ Be specific. Reference actual content from the files. Keep the total response un
136
136
  <div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
137
137
  {/* header row */}
138
138
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.5rem', flexWrap: 'wrap' }}>
139
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
139
+ <span className="font-display" style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>
140
140
  {recentFiles.length > 0
141
141
  ? `${recentFiles.length} recently modified files`
142
142
  : 'Loading recent files…'}
@@ -144,6 +144,7 @@ Be specific. Reference actual content from the files. Keep the total response un
144
144
  <button
145
145
  onClick={generate}
146
146
  disabled={streaming || recentFiles.length === 0}
147
+ className="font-display"
147
148
  style={{
148
149
  display: 'flex',
149
150
  alignItems: 'center',
@@ -151,7 +152,6 @@ Be specific. Reference actual content from the files. Keep the total response un
151
152
  padding: '5px 14px',
152
153
  borderRadius: 7,
153
154
  fontSize: 12,
154
- fontFamily: "'IBM Plex Mono',monospace",
155
155
  cursor: streaming || recentFiles.length === 0 ? 'not-allowed' : 'pointer',
156
156
  border: 'none',
157
157
  background: streaming ? 'var(--muted)' : 'var(--amber)',
@@ -176,6 +176,7 @@ Be specific. Reference actual content from the files. Keep the total response un
176
176
  <a
177
177
  key={f.path}
178
178
  href={`/view/${encodePath(f.path)}`}
179
+ className="font-display"
179
180
  style={{
180
181
  display: 'inline-flex',
181
182
  alignItems: 'center',
@@ -183,7 +184,6 @@ Be specific. Reference actual content from the files. Keep the total response un
183
184
  padding: '3px 10px',
184
185
  borderRadius: 999,
185
186
  fontSize: '0.7rem',
186
- fontFamily: "'IBM Plex Mono',monospace",
187
187
  background: 'var(--muted)',
188
188
  color: 'var(--muted-foreground)',
189
189
  textDecoration: 'none',
@@ -207,7 +207,7 @@ Be specific. Reference actual content from the files. Keep the total response un
207
207
 
208
208
  {/* error */}
209
209
  {error && (
210
- <div style={{ padding: '10px 14px', borderRadius: 8, background: 'rgba(200,60,60,0.1)', border: '1px solid rgba(200,60,60,0.3)', color: '#c83c3c', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12, marginBottom: '1rem' }}>
210
+ <div className="font-display" style={{ padding: '10px 14px', borderRadius: 8, background: 'rgba(200,60,60,0.1)', border: '1px solid rgba(200,60,60,0.3)', color: '#c83c3c', fontSize: 12, marginBottom: '1rem' }}>
211
211
  {error}
212
212
  </div>
213
213
  )}
@@ -235,7 +235,7 @@ Be specific. Reference actual content from the files. Keep the total response un
235
235
  color: 'var(--muted-foreground)',
236
236
  }}>
237
237
  <Sparkles size={28} style={{ margin: '0 auto 10px', opacity: 0.3, color: 'var(--amber)' }} />
238
- <p style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
238
+ <p className="font-display" style={{ fontSize: 12 }}>
239
239
  Click <strong style={{ color: 'var(--foreground)' }}>Generate briefing</strong> to summarize recent changes with AI.
240
240
  </p>
241
241
  </div>
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'summary',
5
+ name: 'AI Briefing',
6
+ description: 'Streams an AI-generated daily briefing summarizing your most recently modified files — key changes, recurring themes, and suggested next actions.',
7
+ author: 'MindOS',
8
+ icon: '✨',
9
+ tags: ['ai', 'summary', 'briefing', 'daily'],
10
+ builtin: true,
11
+ entryPath: 'DAILY.md',
12
+ match: ({ filePath }) => /\b(SUMMARY|summary|Summary|BRIEFING|briefing|Briefing|DAILY|daily|Daily)\b.*\.md$/i.test(filePath),
13
+ load: () => import('./SummaryRenderer').then(m => ({ default: m.SummaryRenderer })),
14
+ };
@@ -71,7 +71,7 @@ function renderInline(text: string): string {
71
71
  return text
72
72
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
73
73
  .replace(/\*(.+?)\*/g, '<em>$1</em>')
74
- .replace(/`(.+?)`/g, '<code style="font-family:\'IBM Plex Mono\',monospace;font-size:0.85em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>')
74
+ .replace(/`(.+?)`/g, '<code class="font-display" style="font-size:0.85em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>')
75
75
  .replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, target, alias) =>
76
76
  `<span style="color:var(--amber);cursor:pointer" title="${target}">${alias ?? target}</span>`)
77
77
  .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:var(--amber)">$1</a>');
@@ -131,7 +131,7 @@ export function TimelineRenderer({ content }: RendererContext) {
131
131
 
132
132
  if (entries.length === 0) {
133
133
  return (
134
- <div style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 13 }}>
134
+ <div className="font-display" style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 13 }}>
135
135
  No timeline entries found. Add <code style={{ background: 'var(--muted)', padding: '1px 6px', borderRadius: 4 }}>## 2025-01-15</code> headings to create entries.
136
136
  </div>
137
137
  );
@@ -141,7 +141,7 @@ export function TimelineRenderer({ content }: RendererContext) {
141
141
  <div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
142
142
  {/* count pill */}
143
143
  <div style={{ marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: 8 }}>
144
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
144
+ <span className="font-display" style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>
145
145
  {entries.length} {entries.length === 1 ? 'entry' : 'entries'}
146
146
  </span>
147
147
  </div>
@@ -176,11 +176,11 @@ export function TimelineRenderer({ content }: RendererContext) {
176
176
  }}>
177
177
  {/* header */}
178
178
  <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
179
- <span style={{ fontFamily: "'IBM Plex Sans',sans-serif", fontWeight: 600, fontSize: '0.9rem', color: 'var(--foreground)' }}>
179
+ <span style={{ fontWeight: 600, fontSize: '0.9rem', color: 'var(--foreground)' }}>
180
180
  {entry.heading}
181
181
  </span>
182
182
  {entry.date && (
183
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: 'var(--muted-foreground)', opacity: 0.7, flexShrink: 0 }}>
183
+ <span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--muted-foreground)', opacity: 0.7, flexShrink: 0 }}>
184
184
  {formatDate(entry.date)}
185
185
  </span>
186
186
  )}
@@ -197,7 +197,7 @@ export function TimelineRenderer({ content }: RendererContext) {
197
197
  {entry.tags.map(tag => {
198
198
  const c = tagColor(tag);
199
199
  return (
200
- <span key={tag} style={{ fontSize: '0.68rem', padding: '1px 8px', borderRadius: 999, fontFamily: "'IBM Plex Mono',monospace", background: c.bg, color: c.text }}>
200
+ <span key={tag} className="font-display" style={{ fontSize: '0.68rem', padding: '1px 8px', borderRadius: 999, background: c.bg, color: c.text }}>
201
201
  #{tag}
202
202
  </span>
203
203
  );
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'timeline',
5
+ name: 'Timeline',
6
+ description: 'Renders changelog and journal files as a vertical timeline. Any markdown with ## date headings (e.g. ## 2025-01-15) becomes a card in the feed.',
7
+ author: 'MindOS',
8
+ icon: '📅',
9
+ tags: ['timeline', 'changelog', 'journal', 'history'],
10
+ builtin: true,
11
+ entryPath: 'CHANGELOG.md',
12
+ match: ({ filePath }) => /\b(CHANGELOG|changelog|TIMELINE|timeline|journal|Journal|diary|Diary)\b.*\.md$/i.test(filePath),
13
+ load: () => import('./TimelineRenderer').then(m => ({ default: m.TimelineRenderer })),
14
+ };
@@ -358,7 +358,7 @@ function SectionCard({
358
358
  >
359
359
  <div className="flex items-center gap-2">
360
360
  <span className={`w-2 h-2 rounded-full shrink-0 ${style.dot}`} />
361
- <span className={`text-xs font-semibold uppercase tracking-wider ${style.label}`} style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
361
+ <span className={`text-xs font-semibold uppercase tracking-wider ${style.label} font-display`}>
362
362
  {name}
363
363
  </span>
364
364
  <span className="text-xs text-muted-foreground">{done}/{total}</span>
@@ -441,7 +441,7 @@ export function TodoRenderer({ content, saveAction }: RendererContext) {
441
441
  <div className="max-w-[900px] mx-auto xl:mr-[220px] px-0 py-2">
442
442
  {/* Summary header */}
443
443
  <div className="mb-6">
444
- <p className="text-xs text-muted-foreground" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
444
+ <p className="text-xs text-muted-foreground font-display">
445
445
  {totalDone} / {totalItems} completed
446
446
  </p>
447
447
  <div className="mt-1.5 w-48 h-1.5 bg-muted rounded-full overflow-hidden">
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'todo',
5
+ name: 'TODO Board',
6
+ description: 'Renders TODO.md/TODO.csv as an interactive kanban board grouped by section. Check items off directly — changes are written back to the source file.',
7
+ author: 'MindOS',
8
+ icon: '✅',
9
+ tags: ['productivity', 'tasks', 'markdown'],
10
+ builtin: true,
11
+ entryPath: 'TODO.md',
12
+ match: ({ filePath }) => /\bTODO\b.*\.(md|csv)$/i.test(filePath),
13
+ load: () => import('./TodoRenderer').then(m => ({ default: m.TodoRenderer })),
14
+ };
@@ -75,7 +75,7 @@ function parseWorkflow(content: string): { meta: WorkflowMeta; steps: WorkflowSt
75
75
  function renderInline(text: string): string {
76
76
  return text
77
77
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
78
- .replace(/`(.+?)`/g, `<code style="font-family:'IBM Plex Mono',monospace;font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>`)
78
+ .replace(/`(.+?)`/g, `<code class="font-display" style="font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>`)
79
79
  .replace(/\*(.+?)\*/g, '<em>$1</em>');
80
80
  }
81
81
 
@@ -188,7 +188,7 @@ function StepCard({
188
188
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '11px 14px' }}>
189
189
  <StatusIcon status={step.status} />
190
190
  <span
191
- style={{ flex: 1, fontFamily: "'IBM Plex Sans',sans-serif", fontWeight: 600, fontSize: '.88rem', color: 'var(--foreground)', cursor: hasBody || hasOutput ? 'pointer' : 'default' }}
191
+ style={{ flex: 1, fontWeight: 600, fontSize: '.88rem', color: 'var(--foreground)', cursor: hasBody || hasOutput ? 'pointer' : 'default' }}
192
192
  onClick={() => (hasBody || hasOutput) && setExpanded(v => !v)}
193
193
  >
194
194
  {step.heading}
@@ -204,7 +204,7 @@ function StepCard({
204
204
  style={{
205
205
  display: 'flex', alignItems: 'center', gap: 4,
206
206
  padding: '3px 10px', borderRadius: 6, fontSize: '0.72rem',
207
- fontFamily: "'IBM Plex Mono',monospace", cursor: canRun ? 'pointer' : 'not-allowed',
207
+ cursor: canRun ? 'pointer' : 'not-allowed',
208
208
  border: 'none', background: canRun ? 'var(--amber)' : 'var(--muted)',
209
209
  color: canRun ? '#131210' : 'var(--muted-foreground)',
210
210
  opacity: canRun ? 1 : 0.5,
@@ -216,7 +216,7 @@ function StepCard({
216
216
  onClick={onSkip}
217
217
  style={{
218
218
  padding: '3px 8px', borderRadius: 6, fontSize: '0.72rem',
219
- fontFamily: "'IBM Plex Mono',monospace", cursor: 'pointer',
219
+ cursor: 'pointer',
220
220
  border: '1px solid var(--border)', background: 'transparent',
221
221
  color: 'var(--muted-foreground)',
222
222
  }}
@@ -226,12 +226,12 @@ function StepCard({
226
226
  </>
227
227
  )}
228
228
  {step.status === 'running' && (
229
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: 'var(--amber)' }}>executing…</span>
229
+ <span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--amber)' }}>executing…</span>
230
230
  )}
231
231
  {(step.status === 'done' || step.status === 'error') && (
232
232
  <button
233
233
  onClick={() => setExpanded(v => !v)}
234
- style={{ padding: '3px 8px', borderRadius: 6, fontSize: '0.72rem', fontFamily: "'IBM Plex Mono',monospace", cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)' }}
234
+ style={{ padding: '3px 8px', borderRadius: 6, fontSize: '0.72rem', cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)' }}
235
235
  >
236
236
  <ChevronDown size={11} style={{ display: 'inline', transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
237
237
  </button>
@@ -251,10 +251,10 @@ function StepCard({
251
251
  <div style={{ padding: '10px 14px', background: 'var(--background)', position: 'relative' }}>
252
252
  <div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 6 }}>
253
253
  <Sparkles size={11} style={{ color: 'var(--amber)' }} />
254
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem', color: 'var(--muted-foreground)', textTransform: 'uppercase', letterSpacing: '.06em' }}>AI Output</span>
254
+ <span className="font-display" style={{ fontSize: '0.68rem', color: 'var(--muted-foreground)', textTransform: 'uppercase', letterSpacing: '.06em' }}>AI Output</span>
255
255
  {step.status === 'running' && <span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--amber)', animation: 'pulse 1.2s ease-in-out infinite', marginLeft: 4 }} />}
256
256
  </div>
257
- <div style={{ fontFamily: "'IBM Plex Sans',sans-serif", fontSize: '.82rem', lineHeight: 1.7, color: 'var(--foreground)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
257
+ <div style={{ fontSize: '.82rem', lineHeight: 1.7, color: 'var(--foreground)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
258
258
  {step.output}
259
259
  </div>
260
260
  </div>
@@ -326,7 +326,7 @@ export function WorkflowRenderer({ filePath, content }: RendererContext) {
326
326
 
327
327
  if (steps.length === 0) {
328
328
  return (
329
- <div style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
329
+ <div className="font-display" style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 12 }}>
330
330
  No steps found. Add <code style={{ background: 'var(--muted)', padding: '1px 5px', borderRadius: 4 }}>## Step N: …</code> headings to define workflow steps.
331
331
  </div>
332
332
  );
@@ -337,7 +337,7 @@ export function WorkflowRenderer({ filePath, content }: RendererContext) {
337
337
  {/* header */}
338
338
  <div style={{ marginBottom: '1.2rem' }}>
339
339
  {parsed.meta.description && (
340
- <p style={{ fontFamily: "'IBM Plex Sans',sans-serif", fontSize: '.82rem', color: 'var(--muted-foreground)', lineHeight: 1.6, marginBottom: 12 }}>
340
+ <p style={{ fontSize: '.82rem', color: 'var(--muted-foreground)', lineHeight: 1.6, marginBottom: 12 }}>
341
341
  {parsed.meta.description}
342
342
  </p>
343
343
  )}
@@ -348,7 +348,7 @@ export function WorkflowRenderer({ filePath, content }: RendererContext) {
348
348
  <div style={{ flex: 1, minWidth: 120, height: 4, borderRadius: 999, background: 'var(--border)', overflow: 'hidden' }}>
349
349
  <div style={{ height: '100%', width: `${progress}%`, background: 'var(--amber)', borderRadius: 999, transition: 'width .3s' }} />
350
350
  </div>
351
- <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: 'var(--muted-foreground)', flexShrink: 0 }}>
351
+ <span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--muted-foreground)', flexShrink: 0 }}>
352
352
  {doneCount}/{steps.length} done
353
353
  </span>
354
354
 
@@ -360,7 +360,7 @@ export function WorkflowRenderer({ filePath, content }: RendererContext) {
360
360
  style={{
361
361
  display: 'flex', alignItems: 'center', gap: 5,
362
362
  padding: '4px 12px', borderRadius: 7, fontSize: '0.75rem',
363
- fontFamily: "'IBM Plex Mono',monospace", cursor: running ? 'not-allowed' : 'pointer',
363
+ cursor: running ? 'not-allowed' : 'pointer',
364
364
  border: 'none', background: running ? 'var(--muted)' : 'var(--amber)',
365
365
  color: running ? 'var(--muted-foreground)' : '#131210',
366
366
  opacity: running ? 0.7 : 1,
@@ -374,7 +374,7 @@ export function WorkflowRenderer({ filePath, content }: RendererContext) {
374
374
  {/* reset */}
375
375
  <button
376
376
  onClick={reset}
377
- style={{ padding: '4px 10px', borderRadius: 7, fontSize: '0.75rem', fontFamily: "'IBM Plex Mono',monospace", cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)', display: 'flex', alignItems: 'center', gap: 4 }}
377
+ style={{ padding: '4px 10px', borderRadius: 7, fontSize: '0.75rem', cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)', display: 'flex', alignItems: 'center', gap: 4 }}
378
378
  >
379
379
  <RotateCcw size={11} /> Reset
380
380
  </button>
@@ -0,0 +1,14 @@
1
+ import type { RendererDefinition } from '@/lib/renderers/registry';
2
+
3
+ export const manifest: RendererDefinition = {
4
+ id: 'workflow',
5
+ name: 'Workflow Runner',
6
+ description: 'Parses step-by-step workflow markdown into an interactive runner. Execute steps sequentially with AI assistance.',
7
+ author: 'MindOS',
8
+ icon: '⚡',
9
+ tags: ['workflow', 'automation', 'steps', 'ai'],
10
+ builtin: true,
11
+ entryPath: 'Workflow.md',
12
+ match: ({ filePath }) => /\b(Workflow|workflow|WORKFLOW)\b.*\.md$/i.test(filePath),
13
+ load: () => import('./WorkflowRenderer').then(m => ({ default: m.WorkflowRenderer })),
14
+ };