@gmickel/gno 0.33.3 → 0.34.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.
@@ -29,6 +29,103 @@ interface ParsedFrontmatter {
29
29
  body: string;
30
30
  }
31
31
 
32
+ const INLINE_ARRAY_REGEX = /^\[([^\]]*)\]$/;
33
+
34
+ function normalizeScalar(value: string): unknown {
35
+ const trimmed = value.trim();
36
+ if (
37
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
38
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
39
+ ) {
40
+ return trimmed.slice(1, -1);
41
+ }
42
+ if (/^-?\d+\.?\d*$/.test(trimmed)) {
43
+ return Number.parseFloat(trimmed);
44
+ }
45
+ if (trimmed === "true") return true;
46
+ if (trimmed === "false") return false;
47
+ return trimmed;
48
+ }
49
+
50
+ function parseYamlFrontmatterBlock(yamlBlock: string): Record<string, unknown> {
51
+ const data: Record<string, unknown> = {};
52
+ const lines = yamlBlock.split("\n");
53
+
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const line = lines[i]?.trimEnd();
56
+ if (!line || line.trimStart().startsWith("#")) {
57
+ continue;
58
+ }
59
+
60
+ const match = line.match(/^([^:]+):\s*(.*)$/);
61
+ if (!match) {
62
+ continue;
63
+ }
64
+
65
+ const [, rawKey = "", rawValue = ""] = match;
66
+ const key = rawKey.trim();
67
+ const value = rawValue.trim();
68
+
69
+ const inlineArrayMatch = INLINE_ARRAY_REGEX.exec(value);
70
+ if (inlineArrayMatch?.[1]) {
71
+ data[key] = inlineArrayMatch[1]
72
+ .split(",")
73
+ .map((item) => normalizeScalar(item))
74
+ .filter((item) => item !== "");
75
+ continue;
76
+ }
77
+
78
+ if (value.length === 0) {
79
+ const arrayItems: unknown[] = [];
80
+ let multilineValue: string[] = [];
81
+ let multilineMode = false;
82
+
83
+ for (let j = i + 1; j < lines.length; j++) {
84
+ const nextLine = lines[j];
85
+ if (nextLine === undefined) {
86
+ break;
87
+ }
88
+
89
+ const trimmedNext = nextLine.trimEnd();
90
+ if (!trimmedNext) {
91
+ if (multilineMode) {
92
+ multilineValue.push("");
93
+ }
94
+ continue;
95
+ }
96
+
97
+ if (!/^\s/.test(nextLine)) {
98
+ break;
99
+ }
100
+
101
+ const arrayMatch = nextLine.match(/^\s*-\s*(.*)$/);
102
+ if (arrayMatch) {
103
+ arrayItems.push(normalizeScalar(arrayMatch[1] ?? ""));
104
+ i = j;
105
+ continue;
106
+ }
107
+
108
+ multilineMode = true;
109
+ multilineValue.push(nextLine.trim());
110
+ i = j;
111
+ }
112
+
113
+ if (arrayItems.length > 0) {
114
+ data[key] = arrayItems;
115
+ } else if (multilineValue.length > 0) {
116
+ data[key] = multilineValue.join("\n");
117
+ } else {
118
+ data[key] = "";
119
+ }
120
+ continue;
121
+ }
122
+
123
+ data[key] = normalizeScalar(value);
124
+ }
125
+
126
+ return data;
127
+ }
128
+
32
129
  /**
33
130
  * Parse YAML frontmatter from markdown content.
34
131
  * Returns empty data if no frontmatter found.
@@ -49,68 +146,25 @@ export function parseFrontmatter(content: string): ParsedFrontmatter {
49
146
 
50
147
  const yamlBlock = trimmed.slice(4, endIndex).trim();
51
148
  const body = trimmed.slice(endIndex + 4).trimStart();
52
-
53
- // Parse YAML manually (simple key: value pairs)
54
- const data: Record<string, unknown> = {};
55
- let currentKey: string | null = null;
56
- let multilineValue: string[] = [];
57
-
58
- for (const line of yamlBlock.split("\n")) {
59
- // Check for multiline continuation (starts with spaces and not a new key)
60
- if (currentKey && line.match(/^\s+/) && !line.includes(":")) {
61
- multilineValue.push(line.trim());
62
- continue;
63
- }
64
-
65
- // Save previous multiline value
66
- if (currentKey && multilineValue.length > 0) {
67
- const existing = data[currentKey];
68
- if (typeof existing === "string" && existing.endsWith("|")) {
69
- data[currentKey] = multilineValue.join("\n");
70
- } else if (typeof existing === "string") {
71
- data[currentKey] = `${existing}\n${multilineValue.join("\n")}`;
72
- } else {
73
- data[currentKey] = multilineValue.join("\n");
74
- }
75
- multilineValue = [];
76
- }
77
-
78
- // Parse new key: value
79
- const match = line.match(/^([^:]+):\s*(.*)$/);
80
- if (match) {
81
- currentKey = match[1].trim();
82
- let value: unknown = match[2].trim();
83
-
84
- // Remove surrounding quotes
85
- if (
86
- (value as string).startsWith('"') &&
87
- (value as string).endsWith('"')
88
- ) {
89
- value = (value as string).slice(1, -1);
90
- }
91
-
92
- // Parse numbers
93
- if (/^-?\d+\.?\d*$/.test(value as string)) {
94
- value = Number.parseFloat(value as string);
95
- }
96
-
97
- data[currentKey] = value;
149
+ if (typeof Bun !== "undefined" && Bun.YAML) {
150
+ try {
151
+ const parsed = Bun.YAML.parse(yamlBlock);
152
+ return {
153
+ data:
154
+ parsed && typeof parsed === "object"
155
+ ? (parsed as Record<string, unknown>)
156
+ : {},
157
+ body,
158
+ };
159
+ } catch {
160
+ // Fall through to the browser-safe parser below.
98
161
  }
99
162
  }
100
163
 
101
- // Handle trailing multiline
102
- if (currentKey && multilineValue.length > 0) {
103
- const existing = data[currentKey];
104
- if (typeof existing === "string" && existing.endsWith("|")) {
105
- data[currentKey] = multilineValue.join("\n");
106
- } else if (typeof existing === "string") {
107
- data[currentKey] = `${existing}\n${multilineValue.join("\n")}`;
108
- } else {
109
- data[currentKey] = multilineValue.join("\n");
110
- }
111
- }
112
-
113
- return { data, body };
164
+ return {
165
+ data: parseYamlFrontmatterBlock(yamlBlock),
166
+ body,
167
+ };
114
168
  }
115
169
 
116
170
  // ─────────────────────────────────────────────────────────────────────────────
@@ -235,9 +289,55 @@ interface ValueDisplayProps {
235
289
  const ValueDisplay: FC<ValueDisplayProps> = ({ keyName, value }) => {
236
290
  // Handle arrays (tags, etc.)
237
291
  if (Array.isArray(value)) {
292
+ const normalizedValues = value.filter(
293
+ (item): item is string | number =>
294
+ typeof item === "string" || typeof item === "number"
295
+ );
296
+
297
+ if (normalizedValues.length === 0) {
298
+ return null;
299
+ }
300
+
301
+ if (isTagsKey(keyName)) {
302
+ return (
303
+ <div className="flex flex-wrap gap-1.5">
304
+ {normalizedValues.map((item, i) => (
305
+ <Badge
306
+ className="rounded-full border border-primary/20 bg-primary/10 px-2 py-0.5 font-mono text-[11px] text-primary"
307
+ key={`${item}-${i}`}
308
+ variant="outline"
309
+ >
310
+ {String(item)}
311
+ </Badge>
312
+ ))}
313
+ </div>
314
+ );
315
+ }
316
+
317
+ if (
318
+ normalizedValues.every((item) => typeof item === "string" && isUrl(item))
319
+ ) {
320
+ return (
321
+ <div className="space-y-2">
322
+ {normalizedValues.map((item, i) => (
323
+ <a
324
+ className="flex max-w-full items-start gap-1 text-primary hover:underline"
325
+ href={String(item)}
326
+ key={`${item}-${i}`}
327
+ rel="noopener noreferrer"
328
+ target="_blank"
329
+ >
330
+ <span className="break-all">{String(item)}</span>
331
+ <ExternalLinkIcon className="mt-0.5 size-3 shrink-0 opacity-60" />
332
+ </a>
333
+ ))}
334
+ </div>
335
+ );
336
+ }
337
+
238
338
  return (
239
- <div className="flex flex-wrap gap-1">
240
- {value.map((item, i) => (
339
+ <div className="flex flex-wrap gap-1.5">
340
+ {normalizedValues.map((item, i) => (
241
341
  <Badge
242
342
  className="font-mono text-xs"
243
343
  key={`${item}-${i}`}
@@ -318,15 +418,15 @@ const FrontmatterItem: FC<FrontmatterItemProps> = ({
318
418
  return (
319
419
  <div
320
420
  className={cn(
321
- "group min-w-0 rounded-lg bg-muted/20 p-3 transition-colors hover:bg-muted/30",
421
+ "group min-w-0 rounded-lg bg-muted/20 p-2.5 transition-colors hover:bg-muted/30",
322
422
  isLarge && "col-span-full"
323
423
  )}
324
424
  >
325
- <div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground text-xs">
425
+ <div className="mb-1 flex items-center gap-1.5 text-muted-foreground text-[11px]">
326
426
  {icon}
327
427
  <span className="uppercase tracking-wider">{formatKey(keyName)}</span>
328
428
  </div>
329
- <div className="text-sm">
429
+ <div className="text-sm leading-relaxed">
330
430
  <ValueDisplay keyName={keyName} value={value} />
331
431
  </div>
332
432
  </div>
@@ -250,41 +250,37 @@ export function OutgoingLinksPanel({
250
250
 
251
251
  return (
252
252
  <Collapsible
253
- className={cn(
254
- // Container styling - dark manuscript edge
255
- "border-border/40 border-l",
256
- "bg-gradient-to-b from-[#050505] to-[#0a0a0a]",
257
- className
258
- )}
253
+ className={cn("px-1", className)}
259
254
  onOpenChange={setIsOpen}
260
255
  open={isOpen}
261
256
  >
262
257
  {/* Header trigger */}
263
258
  <CollapsibleTrigger
264
259
  className={cn(
265
- "group flex w-full items-center gap-2 px-3 py-2.5",
260
+ "group flex w-full items-center gap-2",
261
+ "rounded-sm px-2 py-1.5",
266
262
  "transition-colors duration-150",
267
- "hover:bg-[#4db8a8]/5"
263
+ "hover:bg-muted/20"
268
264
  )}
269
265
  >
270
266
  {/* Chevron */}
271
267
  <ChevronDownIcon
272
268
  className={cn(
273
- "size-4 shrink-0 text-muted-foreground/60",
269
+ "size-3.5 shrink-0 text-muted-foreground/50",
274
270
  "transition-transform duration-200",
275
271
  !isOpen && "-rotate-90"
276
272
  )}
277
273
  />
278
274
 
279
275
  {/* Title */}
280
- <span className="flex-1 text-left font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
276
+ <span className="flex-1 text-left font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
281
277
  Outgoing Links
282
278
  </span>
283
279
 
284
280
  {/* Count badges */}
285
281
  {!loading && links.length > 0 && (
286
282
  <div className="flex items-center gap-1.5">
287
- <span className="rounded bg-[#4db8a8]/15 px-1.5 py-0.5 font-mono text-[10px] tabular-nums text-[#4db8a8]">
283
+ <span className="rounded bg-primary/12 px-1.5 py-0.5 font-mono text-[10px] tabular-nums text-primary">
288
284
  {links.length}
289
285
  </span>
290
286
  {brokenCount > 0 && (
@@ -295,7 +291,7 @@ export function OutgoingLinksPanel({
295
291
  </div>
296
292
  )}
297
293
 
298
- {/* Loading indicator in header */}
294
+ {/* Loading indicator */}
299
295
  {loading && (
300
296
  <Loader2Icon className="size-3.5 animate-spin text-muted-foreground/50" />
301
297
  )}
@@ -190,34 +190,17 @@ function SimilarityBar({ score }: { score: number }) {
190
190
  const percentage = Math.round(score * 100);
191
191
 
192
192
  return (
193
- <div className="group/bar relative mt-1.5">
194
- {/* Label */}
195
- <div className="mb-0.5 flex items-center justify-between">
196
- <span className="font-mono text-[9px] text-muted-foreground/60 uppercase tracking-wider">
197
- Similarity
198
- </span>
199
- <span className="font-mono text-[10px] text-primary/80 tabular-nums">
200
- {percentage}%
201
- </span>
202
- </div>
203
-
204
- {/* Bar track - glass effect */}
205
- <div className="relative h-1.5 overflow-hidden rounded-full bg-muted/30 shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)]">
206
- {/* Fill - teal glow */}
193
+ <div className="mt-1 flex items-center gap-2">
194
+ {/* Bar track */}
195
+ <div className="relative h-1 flex-1 overflow-hidden rounded-full bg-muted/30">
207
196
  <div
208
- className={cn(
209
- "absolute inset-y-0 left-0 rounded-full",
210
- "bg-gradient-to-r from-primary/70 via-primary to-primary/80",
211
- "transition-all duration-500 ease-out",
212
- // Subtle glow on high scores
213
- score > 0.7 && "shadow-[0_0_8px_hsl(var(--primary)/0.5)]"
214
- )}
197
+ className="absolute inset-y-0 left-0 rounded-full bg-primary/60"
215
198
  style={{ width: `${percentage}%` }}
216
- >
217
- {/* Shimmer effect */}
218
- <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent opacity-0 transition-opacity duration-300 group-hover/bar:opacity-100" />
219
- </div>
199
+ />
220
200
  </div>
201
+ <span className="shrink-0 font-mono text-[9px] text-muted-foreground/50 tabular-nums">
202
+ {percentage}%
203
+ </span>
221
204
  </div>
222
205
  );
223
206
  }
@@ -238,16 +221,13 @@ function RelatedNoteItem({
238
221
  return (
239
222
  <button
240
223
  className={cn(
241
- // Base - specimen card feel
242
- "group relative w-full rounded-sm text-left",
243
- "border border-border/40 bg-card/50",
244
- "p-2.5 transition-all duration-200",
245
- // Hover state - lift and glow
246
- "hover:border-primary/30 hover:bg-card/80",
247
- "hover:shadow-[0_2px_12px_-4px_hsl(var(--primary)/0.2)]",
248
- // Focus state
249
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50",
250
- // Stagger animation
224
+ "group relative flex min-w-0 w-full items-start gap-2",
225
+ "rounded px-2 py-1.5 text-left",
226
+ "font-mono text-xs",
227
+ "transition-all duration-150",
228
+ "text-primary/90 hover:bg-muted/20",
229
+ "cursor-pointer hover:translate-x-0.5",
230
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50",
251
231
  "animate-fade-in opacity-0"
252
232
  )}
253
233
  onClick={onNavigate}
@@ -257,43 +237,29 @@ function RelatedNoteItem({
257
237
  }}
258
238
  type="button"
259
239
  >
260
- {/* Title with tooltip for long names */}
240
+ <span
241
+ className={cn(
242
+ "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded",
243
+ "bg-primary/15 transition-colors duration-150",
244
+ "group-hover:bg-primary/25"
245
+ )}
246
+ >
247
+ <SparklesIcon className="size-3" />
248
+ </span>
249
+
261
250
  <Tooltip>
262
251
  <TooltipTrigger asChild>
263
- <h4 className="line-clamp-3 break-words font-mono text-[13px] leading-tight text-foreground/90 group-hover:text-foreground">
264
- {doc.title || "Untitled"}
265
- </h4>
252
+ <div className="min-w-0 flex-1">
253
+ <span className="block break-words font-medium leading-tight whitespace-normal text-foreground/90 group-hover:text-foreground">
254
+ {doc.title || "Untitled"}
255
+ </span>
256
+ <SimilarityBar score={doc.score} />
257
+ </div>
266
258
  </TooltipTrigger>
267
259
  <TooltipContent side="left" className="max-w-[300px]">
268
260
  <p className="break-words">{doc.title || "Untitled"}</p>
269
261
  </TooltipContent>
270
262
  </Tooltip>
271
-
272
- {/* Collection badge - brass plate style */}
273
- <div className="mt-1 flex items-center gap-1.5">
274
- <span
275
- className={cn(
276
- "inline-flex items-center gap-1 rounded px-1.5 py-0.5",
277
- "bg-secondary/10 font-mono text-[10px] text-secondary/80",
278
- "transition-colors group-hover:bg-secondary/15 group-hover:text-secondary"
279
- )}
280
- >
281
- {doc.collection}
282
- </span>
283
- </div>
284
-
285
- {/* Similarity bar */}
286
- <SimilarityBar score={doc.score} />
287
-
288
- {/* Hover indicator - brass accent */}
289
- <div
290
- className={cn(
291
- "absolute right-2 top-2 opacity-0 transition-opacity",
292
- "group-hover:opacity-100"
293
- )}
294
- >
295
- <SparklesIcon className="size-3.5 text-secondary/60" />
296
- </div>
297
263
  </button>
298
264
  );
299
265
  }
@@ -406,19 +372,19 @@ export function RelatedNotesSidebar({
406
372
  }
407
373
 
408
374
  return (
409
- <div className={cn("space-y-1", className)}>
375
+ <div className={cn("min-w-0 overflow-hidden px-1", className)}>
410
376
  <Collapsible onOpenChange={setIsOpen} open={isOpen}>
411
377
  {/* Header */}
412
- <div className="flex items-center gap-1 px-2 py-1">
378
+ <div className="flex items-center gap-1">
413
379
  <CollapsibleTrigger
414
380
  className={cn(
415
- "flex flex-1 items-center gap-1.5 rounded-sm px-1.5 py-1",
381
+ "flex flex-1 items-center gap-2 rounded-sm px-2 py-1.5",
416
382
  "transition-colors duration-150",
417
- "hover:bg-muted/30"
383
+ "hover:bg-muted/20"
418
384
  )}
419
385
  >
420
386
  {/* Chevron */}
421
- <span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground/60 transition-transform duration-200">
387
+ <span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground/50 transition-transform duration-200">
422
388
  {isOpen ? (
423
389
  <ChevronDownIcon className="size-3.5" />
424
390
  ) : (
@@ -427,13 +393,13 @@ export function RelatedNotesSidebar({
427
393
  </span>
428
394
 
429
395
  {/* Title */}
430
- <span className="flex-1 text-left font-mono text-[11px] text-muted-foreground uppercase tracking-wider">
396
+ <span className="flex-1 text-left font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
431
397
  Related Notes
432
398
  </span>
433
399
 
434
400
  {/* Count badge */}
435
401
  {similar.length > 0 && (
436
- <span className="rounded bg-primary/10 px-1.5 py-0.5 font-mono text-[10px] text-primary/80 tabular-nums">
402
+ <span className="rounded bg-primary/12 px-1.5 py-0.5 font-mono text-[10px] text-primary tabular-nums">
437
403
  {similar.length}
438
404
  </span>
439
405
  )}
@@ -442,9 +408,9 @@ export function RelatedNotesSidebar({
442
408
  {/* Hide button */}
443
409
  <button
444
410
  className={cn(
445
- "flex size-6 items-center justify-center rounded-sm",
446
- "text-muted-foreground/50 transition-colors",
447
- "hover:bg-muted/30 hover:text-muted-foreground"
411
+ "flex size-5 items-center justify-center rounded-sm",
412
+ "text-muted-foreground/40 transition-colors",
413
+ "hover:bg-muted/20 hover:text-muted-foreground"
448
414
  )}
449
415
  onClick={handleToggleVisibility}
450
416
  title="Hide related notes"
@@ -472,7 +438,7 @@ export function RelatedNotesSidebar({
472
438
  ) : similar.length === 0 ? (
473
439
  <RelatedNotesEmpty />
474
440
  ) : (
475
- <div className="space-y-1.5 p-2">
441
+ <div className="space-y-0.5 p-2">
476
442
  {similar.map((doc, index) => (
477
443
  <RelatedNoteItem
478
444
  doc={doc}
@@ -13,10 +13,16 @@ import ReactMarkdown from "react-markdown";
13
13
  import rehypeSanitize from "rehype-sanitize";
14
14
  import remarkGfm from "remark-gfm";
15
15
 
16
+ import {
17
+ normalizeWikiName,
18
+ parseTargetParts,
19
+ stripWikiMdExt,
20
+ } from "../../../../core/links";
16
21
  import {
17
22
  extractMarkdownCodeLanguage,
18
23
  resolveCodeLanguage,
19
24
  } from "../../lib/code-language";
25
+ import { buildDocDeepLink } from "../../lib/deep-links";
20
26
  import { cn } from "../../lib/utils";
21
27
  import { CodeBlock, CodeBlockCopyButton } from "../ai-elements/code-block";
22
28
 
@@ -25,6 +31,53 @@ export interface MarkdownPreviewProps {
25
31
  content: string;
26
32
  /** Additional CSS classes */
27
33
  className?: string;
34
+ /** Current collection for wiki-link resolution */
35
+ collection?: string;
36
+ /** Resolved outgoing wiki links for the current document */
37
+ wikiLinks?: Array<{
38
+ targetRef: string;
39
+ targetCollection?: string;
40
+ targetAnchor?: string;
41
+ resolvedUri?: string;
42
+ }>;
43
+ }
44
+
45
+ const WIKI_LINK_REGEX = /\[\[([^\]|]+(?:\|[^\]]+)?)\]\]/g;
46
+
47
+ function renderMarkdownWithWikiLinks(
48
+ content: string,
49
+ collection?: string,
50
+ wikiLinks?: MarkdownPreviewProps["wikiLinks"]
51
+ ): string {
52
+ if (!content.includes("[[")) {
53
+ return content;
54
+ }
55
+
56
+ const resolvedWikiLinkMap = new Map<string, string>();
57
+ for (const link of wikiLinks ?? []) {
58
+ const targetCollection = link.targetCollection || collection || "";
59
+ const targetRefKey = normalizeWikiName(stripWikiMdExt(link.targetRef));
60
+ const targetAnchorKey = (link.targetAnchor ?? "").trim().toLowerCase();
61
+ const key = `${targetCollection}::${targetRefKey}::${targetAnchorKey}`;
62
+ if (link.resolvedUri) {
63
+ resolvedWikiLinkMap.set(key, buildDocDeepLink({ uri: link.resolvedUri }));
64
+ }
65
+ }
66
+
67
+ return content.replace(WIKI_LINK_REGEX, (match, rawContent: string) => {
68
+ const [rawTarget, rawAlias] = rawContent.split("|");
69
+ const displayText = rawAlias?.trim() || rawTarget?.trim() || match;
70
+ const parsed = parseTargetParts(rawTarget ?? "");
71
+ const targetCollection = parsed.collection || collection || "";
72
+ const targetRefKey = normalizeWikiName(stripWikiMdExt(parsed.ref));
73
+ const targetAnchorKey = (parsed.anchor ?? "").trim().toLowerCase();
74
+ const key = `${targetCollection}::${targetRefKey}::${targetAnchorKey}`;
75
+ const href =
76
+ resolvedWikiLinkMap.get(key) ||
77
+ `/search?query=${encodeURIComponent(stripWikiMdExt(parsed.ref))}`;
78
+
79
+ return `[${displayText}](${href})`;
80
+ });
28
81
  }
29
82
 
30
83
  // Inline code styling
@@ -319,7 +372,7 @@ const components = {
319
372
  * Sanitizes HTML to prevent XSS attacks.
320
373
  */
321
374
  export const MarkdownPreview = memo(
322
- ({ content, className }: MarkdownPreviewProps) => {
375
+ ({ content, className, collection, wikiLinks }: MarkdownPreviewProps) => {
323
376
  if (!content) {
324
377
  return (
325
378
  <div className={cn("text-muted-foreground italic", className)}>
@@ -328,6 +381,12 @@ export const MarkdownPreview = memo(
328
381
  );
329
382
  }
330
383
 
384
+ const renderedContent = renderMarkdownWithWikiLinks(
385
+ content,
386
+ collection,
387
+ wikiLinks
388
+ );
389
+
331
390
  return (
332
391
  <div
333
392
  className={cn(
@@ -342,7 +401,7 @@ export const MarkdownPreview = memo(
342
401
  rehypePlugins={[rehypeSanitize]}
343
402
  remarkPlugins={[remarkGfm]}
344
403
  >
345
- {content}
404
+ {renderedContent}
346
405
  </ReactMarkdown>
347
406
  </div>
348
407
  );
@@ -6,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority";
6
6
  import { cn } from "../../lib/utils";
7
7
 
8
8
  const buttonVariants = cva(
9
- "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
9
+ "inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
10
10
  {
11
11
  variants: {
12
12
  variant: {
@@ -67,7 +67,7 @@ function DialogContent({
67
67
  {children}
68
68
  {showCloseButton && (
69
69
  <DialogPrimitive.Close
70
- className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
70
+ className="absolute top-4 right-4 cursor-pointer rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
71
71
  data-slot="dialog-close"
72
72
  >
73
73
  <XIcon />
@@ -0,0 +1,38 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ import type {
4
+ WorkspaceTab,
5
+ WorkspaceTabBrowseState,
6
+ } from "../lib/workspace-tabs";
7
+
8
+ interface WorkspaceContextValue {
9
+ activeTab: WorkspaceTab | null;
10
+ updateActiveTabBrowseState: (
11
+ nextBrowseState:
12
+ | WorkspaceTabBrowseState
13
+ | ((current: WorkspaceTabBrowseState) => WorkspaceTabBrowseState)
14
+ ) => void;
15
+ }
16
+
17
+ const WorkspaceContext = createContext<WorkspaceContextValue>({
18
+ activeTab: null,
19
+ updateActiveTabBrowseState: () => undefined,
20
+ });
21
+
22
+ export function WorkspaceProvider({
23
+ children,
24
+ value,
25
+ }: {
26
+ children: React.ReactNode;
27
+ value: WorkspaceContextValue;
28
+ }) {
29
+ return (
30
+ <WorkspaceContext.Provider value={value}>
31
+ {children}
32
+ </WorkspaceContext.Provider>
33
+ );
34
+ }
35
+
36
+ export function useWorkspace() {
37
+ return useContext(WorkspaceContext);
38
+ }