wiki-viewer 1.0.0 → 1.0.1

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 (128) hide show
  1. package/.next/standalone/README.md +328 -99
  2. package/.next/standalone/agents/bootstrap-prompt.md +1 -0
  3. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
  4. package/.next/standalone/bin/wiki-viewer.js +57 -4
  5. package/.next/standalone/docs/agent-collab-plan.md +1615 -0
  6. package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
  7. package/.next/standalone/package.json +19 -2
  8. package/.next/standalone/pnpm-lock.yaml +1368 -325
  9. package/.next/standalone/pnpm-workspace.yaml +7 -1
  10. package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
  11. package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
  12. package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
  13. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
  14. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
  15. package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
  16. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
  17. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
  18. package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
  19. package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
  20. package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
  21. package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
  22. package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
  23. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
  24. package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
  25. package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
  26. package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
  27. package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
  28. package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
  29. package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
  30. package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
  31. package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
  32. package/.next/standalone/src/app/api/system/config/route.ts +5 -1
  33. package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
  34. package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
  35. package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
  36. package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
  37. package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
  38. package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
  39. package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
  40. package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
  41. package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
  42. package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
  43. package/.next/standalone/src/app/api/wiki/route.ts +10 -0
  44. package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
  45. package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
  46. package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
  47. package/.next/standalone/src/app/globals.css +70 -0
  48. package/.next/standalone/src/app/page.tsx +461 -217
  49. package/.next/standalone/src/app/signin/page.tsx +217 -0
  50. package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
  51. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
  52. package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
  53. package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
  54. package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
  55. package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
  56. package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
  57. package/.next/standalone/src/components/editor/editor.tsx +513 -10
  58. package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
  59. package/.next/standalone/src/components/editor/extensions.ts +2 -0
  60. package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
  61. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
  62. package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
  63. package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
  64. package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
  65. package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
  66. package/.next/standalone/src/lib/auth/client.ts +4 -0
  67. package/.next/standalone/src/lib/auth/csrf.ts +68 -0
  68. package/.next/standalone/src/lib/auth/server.ts +164 -0
  69. package/.next/standalone/src/lib/config.ts +4 -0
  70. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
  71. package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
  72. package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
  73. package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
  74. package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
  75. package/.next/standalone/src/lib/proof/activity.ts +74 -0
  76. package/.next/standalone/src/lib/proof/auth.ts +132 -0
  77. package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
  78. package/.next/standalone/src/lib/proof/blocks.ts +73 -0
  79. package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
  80. package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
  81. package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
  82. package/.next/standalone/src/lib/proof/glob.ts +51 -0
  83. package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
  84. package/.next/standalone/src/lib/proof/mutex.ts +32 -0
  85. package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
  86. package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
  87. package/.next/standalone/src/lib/proof/pending.ts +97 -0
  88. package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
  89. package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
  90. package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
  91. package/.next/standalone/src/lib/proof/registry.ts +190 -0
  92. package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
  93. package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
  94. package/.next/standalone/src/lib/proof/types.ts +170 -0
  95. package/.next/standalone/src/lib/proof-config.ts +14 -0
  96. package/.next/standalone/src/middleware.ts +38 -0
  97. package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
  98. package/.next/standalone/src/stores/editor-store.ts +115 -9
  99. package/.next/standalone/src/stores/proof-store.ts +123 -0
  100. package/.next/standalone/src/stores/view-width-store.ts +62 -0
  101. package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
  102. package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
  103. package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
  104. package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
  105. package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
  106. package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
  107. package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
  108. package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
  109. package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
  110. package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
  111. package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
  112. package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
  113. package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
  114. package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
  115. package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
  116. package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
  117. package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
  118. package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
  119. package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
  120. package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
  121. package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
  122. package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
  123. package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
  124. package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
  125. package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
  126. package/README.md +328 -99
  127. package/bin/wiki-viewer.js +57 -4
  128. package/package.json +19 -2
@@ -2,10 +2,12 @@
2
2
 
3
3
  import {
4
4
  AlertCircle,
5
+ Bot,
5
6
  Check,
6
7
  ChevronDown,
7
8
  ChevronRight,
8
9
  File,
10
+ FilePlus,
9
11
  FileText,
10
12
  Folder,
11
13
  FolderOpen,
@@ -17,15 +19,16 @@ import {
17
19
  PanelLeftClose,
18
20
  PanelLeftOpen,
19
21
  Pencil,
22
+ Plus,
20
23
  RefreshCw,
24
+ Settings,
21
25
  Terminal,
22
26
  Trash2,
23
27
  Upload,
24
28
  X,
25
29
  } from "lucide-react";
26
30
  import { useCallback, useEffect, useRef, useState } from "react";
27
- import ReactMarkdown from "react-markdown";
28
- import remarkGfm from "remark-gfm";
31
+
29
32
  import { ConfirmDialog } from "@/components/confirm-dialog";
30
33
  import { CsvViewer } from "@/components/editor/csv-viewer";
31
34
  import { KBEditor } from "@/components/editor/editor";
@@ -42,15 +45,27 @@ import { SourceViewer } from "@/components/editor/source-viewer";
42
45
  import { WebsiteViewer } from "@/components/editor/website-viewer";
43
46
  import { NodeAppViewer } from "@/components/editor/node-app-viewer";
44
47
  import { DirPicker } from "@/components/dir-picker";
48
+ import {
49
+ DropdownMenu,
50
+ DropdownMenuContent,
51
+ DropdownMenuItem,
52
+ DropdownMenuTrigger,
53
+ } from "@/components/ui/dropdown-menu";
45
54
  import { ThemeToggle } from "@/components/theme-toggle";
55
+ import { AuthSettingsSheet } from "@/components/auth-settings-sheet";
46
56
  import { Button } from "@/components/ui/button";
47
57
  import { Card } from "@/components/ui/card";
48
58
  import { FrontmatterHeader } from "@/components/wiki/frontmatter-header";
59
+ import { MarkdownPreview } from "@/components/wiki/markdown-preview";
49
60
  import { parseFrontmatter } from "@/lib/markdown/parse-frontmatter";
50
- import remarkWikilinks from "@/lib/markdown/remark-wikilinks";
61
+
51
62
  import { showError } from "@/lib/toast";
52
63
  import { cn } from "@/lib/utils";
64
+ import { AIPanel } from "@/components/ai-panel/ai-panel";
65
+ import { useAIPanelStore } from "@/stores/ai-panel-store";
53
66
  import { useEditorStore } from "@/stores/editor-store";
67
+ import { useViewWidthStore, VIEW_WIDTH_CLASS } from "@/stores/view-width-store";
68
+ import { ViewWidthToggle } from "@/components/view-width-toggle";
54
69
  import { useWikiSlugsStore } from "@/stores/wiki-slugs-store";
55
70
 
56
71
  interface TreeNode {
@@ -128,9 +143,21 @@ function viewerKindFor(
128
143
  return "fallback";
129
144
  }
130
145
 
146
+ const TEXT_EDITABLE_EXTS = new Set([
147
+ "txt", "md", "markdown", "json", "yaml", "yml", "toml", "csv", "tsv",
148
+ "xml", "html", "css", "js", "ts", "tsx", "jsx", "sh", "bash", "zsh",
149
+ "rb", "py", "go", "rs", "java", "c", "cpp", "h", "php", "swift", "kt",
150
+ "lua", "sql", "scss", "mmd", "mermaid", "ini", "env", "log", "conf",
151
+ ]);
152
+
131
153
  function isText(name: string) {
132
154
  const kind = viewerKindFor(name, "file");
133
- return kind === "editor" || kind === "text";
155
+ if (kind === "editor" || kind === "text") return true;
156
+ return TEXT_EDITABLE_EXTS.has(ext(name));
157
+ }
158
+
159
+ function isMarkdown(name: string) {
160
+ return ["md", "markdown"].includes(ext(name));
134
161
  }
135
162
  function isImage(name: string) {
136
163
  return viewerKindFor(name, "file") === "image";
@@ -204,6 +231,15 @@ export default function Page() {
204
231
 
205
232
  const editorCurrentPath = useEditorStore((s) => s.currentPath);
206
233
 
234
+ // Path captured from the URL at first render, before any effect can clear it.
235
+ // Restore reads from this ref (never the live URL) so URL sync can't break it.
236
+ const initialUrlPathRef = useRef<string | null>(
237
+ typeof window !== "undefined"
238
+ ? new URLSearchParams(window.location.search).get("path")
239
+ : null,
240
+ );
241
+ const didRestoreRef = useRef(false);
242
+
207
243
  const [roots, setRoots] = useState<TreeNode[]>([]);
208
244
  const [rootLoaded, setRootLoaded] = useState(false);
209
245
  const [rootLoading, setRootLoading] = useState(false);
@@ -219,6 +255,10 @@ export default function Page() {
219
255
  const [newFolderName, setNewFolderName] = useState("");
220
256
  const [folderError, setFolderError] = useState<string | null>(null);
221
257
 
258
+ const [newFileParent, setNewFileParent] = useState<string | null>(null);
259
+ const [newFileName, setNewFileName] = useState("");
260
+ const [fileCreateError, setFileCreateError] = useState<string | null>(null);
261
+
222
262
  const [deletingPath, setDeletingPath] = useState<string | null>(null);
223
263
  const [deletingIsDir, setDeletingIsDir] = useState(false);
224
264
 
@@ -230,6 +270,7 @@ export default function Page() {
230
270
  const [appFullscreen, setAppFullscreen] = useState(false);
231
271
  const [appKey, setAppKey] = useState(0);
232
272
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
273
+ const [settingsOpen, setSettingsOpen] = useState(false);
233
274
  const [fileContent, setFileContent] = useState<string | null>(null);
234
275
  const [fileLoading, setFileLoading] = useState(false);
235
276
  const [editing, setEditing] = useState(false);
@@ -263,10 +304,58 @@ export default function Page() {
263
304
  });
264
305
  }, [rootLoaded]);
265
306
 
307
+ // Expand every ancestor folder of `p` so the file is visible in the tree.
308
+ // Best-effort: requires the root nodes to already be loaded.
309
+ const revealPath = useCallback(async (p: string) => {
310
+ const parts = p.split("/");
311
+ if (parts.length <= 1) return;
312
+
313
+ // Fetch every ancestor's children first. We must NOT apply them with one
314
+ // setRoots per level: those updaters run against the same stale `prev`
315
+ // snapshot, so a nested prefix isn't in the tree yet and updateNodes
316
+ // silently no-ops (only the top level ever matched). Collect all levels,
317
+ // then splice them in a single pass so each parent's children exist before
318
+ // the next level is inserted.
319
+ const levels: { prefix: string; children: TreeNode[] }[] = [];
320
+ let prefix = "";
321
+ for (let i = 0; i < parts.length - 1; i++) {
322
+ prefix = prefix ? `${prefix}/${parts[i]}` : parts[i];
323
+ levels.push({ prefix, children: await fetchDir(prefix) });
324
+ }
325
+
326
+ setRoots((prev) => {
327
+ let next = prev;
328
+ for (const { prefix: pfx, children } of levels) {
329
+ next = updateNodes(next, pfx, (n) => ({
330
+ ...n,
331
+ children,
332
+ expanded: true,
333
+ }));
334
+ }
335
+ return next;
336
+ });
337
+ }, []);
338
+
266
339
  const reloadDir = useCallback(async (dir: string) => {
267
340
  const fresh = await fetchDir(dir);
268
341
  if (dir === "") {
269
- setRoots(fresh);
342
+ // Merge: keep expanded state + loaded children for nodes that still
343
+ // exist. A blind setRoots(fresh) would collapse the tree and wipe any
344
+ // expansion done by revealPath on reload (watcher fires this often).
345
+ setRoots((prev) => {
346
+ const prevByPath = new Map(prev.map((n) => [n.path, n]));
347
+ return fresh.map((n) => {
348
+ const old = prevByPath.get(n.path);
349
+ if (old && (old.type === "dir" || old.type === "app")) {
350
+ return {
351
+ ...n,
352
+ expanded: old.expanded,
353
+ children: old.children,
354
+ };
355
+ }
356
+ return n;
357
+ });
358
+ });
270
359
  } else {
271
360
  setRoots((prev) =>
272
361
  updateNodes(prev, dir, (n) => ({
@@ -313,7 +402,7 @@ export default function Page() {
313
402
  const refreshViewer = useCallback(async () => {
314
403
  if (!openFile) return;
315
404
  const kind = viewerKindFor(openFile.name, openFile.nodeType);
316
- if (!["editor", "text"].includes(kind)) return;
405
+ if (!["editor", "text"].includes(kind) && !isText(openFile.name)) return;
317
406
  setFileLoading(true);
318
407
  try {
319
408
  const res = await fetch(
@@ -448,7 +537,7 @@ export default function Page() {
448
537
  setSaveError(null);
449
538
  setFileContent(null);
450
539
  const kind = viewerKindFor(node.name, node.type);
451
- if (!["editor", "text"].includes(kind)) return;
540
+ if (!["editor", "text"].includes(kind) && !isText(node.name)) return;
452
541
  setFileLoading(true);
453
542
  try {
454
543
  const res = await fetch(
@@ -476,6 +565,46 @@ export default function Page() {
476
565
  } as TreeNode);
477
566
  }, [editorCurrentPath]);
478
567
 
568
+ // Persist the open file to the URL (?path=) so reloads restore it.
569
+ // replaceState avoids polluting history on every file switch.
570
+ useEffect(() => {
571
+ if (typeof window === "undefined") return;
572
+ const url = new URL(window.location.href);
573
+ if (openFile) url.searchParams.set("path", openFile.path);
574
+ else url.searchParams.delete("path");
575
+ window.history.replaceState(null, "", url.toString());
576
+ }, [openFile]);
577
+
578
+ // Restore the open file from the URL once the root tree is loaded.
579
+ // Resolves node type from the parent dir listing (file vs app vs node-app).
580
+ // biome-ignore lint/correctness/useExhaustiveDependencies: openViewer is a hoisted stable fn; restore runs once
581
+ useEffect(() => {
582
+ if (didRestoreRef.current) return;
583
+ if (!rootLoaded) return;
584
+ const target = initialUrlPathRef.current;
585
+ if (!target) {
586
+ didRestoreRef.current = true;
587
+ return;
588
+ }
589
+ didRestoreRef.current = true;
590
+
591
+ void (async () => {
592
+ const parts = target.split("/");
593
+ const name = parts[parts.length - 1];
594
+ const parentDir = parts.slice(0, -1).join("/");
595
+ const siblings = await fetchDir(parentDir);
596
+ const match = siblings.find((s) => s.path === target);
597
+ if (!match) return; // file no longer exists; leave home view
598
+ await revealPath(target);
599
+ void openViewer({
600
+ path: target,
601
+ name,
602
+ type: match.type,
603
+ modifiedAt: match.modifiedAt,
604
+ } as TreeNode);
605
+ })();
606
+ }, [rootLoaded, revealPath]);
607
+
479
608
  const handleChangeDir = async () => {
480
609
  await fetch("/api/system/clear-root", { method: "POST" });
481
610
  setOpenFile(null);
@@ -570,6 +699,39 @@ export default function Page() {
570
699
  }
571
700
  }
572
701
 
702
+ async function handleCreateFile() {
703
+ const raw = newFileName.trim();
704
+ if (!raw || newFileParent === null) return;
705
+ setFileCreateError(null);
706
+ const name = raw.includes(".") ? raw : `${raw}.md`;
707
+ const rel = newFileParent ? `${newFileParent}/${name}` : name;
708
+ const res = await fetch("/api/wiki/new-file", {
709
+ method: "POST",
710
+ headers: { "Content-Type": "application/json" },
711
+ body: JSON.stringify({ path: rel }),
712
+ });
713
+ if (res.ok) {
714
+ const parent = newFileParent;
715
+ setNewFileParent(null);
716
+ setNewFileName("");
717
+ await reloadDir(parent);
718
+ if (parent !== "") {
719
+ setRoots((prev) =>
720
+ updateNodes(prev, parent, (n) => ({ ...n, expanded: true })),
721
+ );
722
+ }
723
+ void openViewer({
724
+ path: rel,
725
+ name,
726
+ type: "file",
727
+ modifiedAt: new Date().toISOString(),
728
+ } as TreeNode);
729
+ } else {
730
+ const e: { error?: string } = await res.json();
731
+ setFileCreateError(e.error ?? "Failed");
732
+ }
733
+ }
734
+
573
735
  async function handleDelete() {
574
736
  if (!deletingPath) return;
575
737
  await fetch("/api/wiki", {
@@ -652,6 +814,17 @@ export default function Page() {
652
814
  ? viewerKindFor(openFile.name, openFile.nodeType)
653
815
  : null;
654
816
 
817
+ const viewWidth = useViewWidthStore((s) => s.width);
818
+ // Width toggle only meaningful for text-flow viewers (long prose lines).
819
+ const widthAwareViewer =
820
+ openFileViewerKind === null ||
821
+ openFileViewerKind === "editor" ||
822
+ openFileViewerKind === "text" ||
823
+ openFileViewerKind === "source" ||
824
+ openFileViewerKind === "notebook" ||
825
+ openFileViewerKind === "fallback";
826
+ const contentWidthClass = widthAwareViewer ? VIEW_WIDTH_CLASS[viewWidth] : "";
827
+
655
828
  function renderNodes(nodes: TreeNode[], depth = 0): React.ReactNode {
656
829
  return nodes.map((node) => (
657
830
  <div key={node.path}>
@@ -735,6 +908,20 @@ export default function Page() {
735
908
  >
736
909
  {node.type === "dir" && (
737
910
  <>
911
+ <Button
912
+ size="sm"
913
+ variant="ghost"
914
+ className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
915
+ title="New file here (default .md)"
916
+ onClick={async () => {
917
+ if (!node.expanded) await toggleFolder(node);
918
+ setNewFileParent(node.path);
919
+ setNewFileName("");
920
+ setFileCreateError(null);
921
+ }}
922
+ >
923
+ <FilePlus className="h-3 w-3" />
924
+ </Button>
738
925
  <Button
739
926
  size="sm"
740
927
  variant="ghost"
@@ -819,6 +1006,52 @@ export default function Page() {
819
1006
  </div>
820
1007
  )}
821
1008
 
1009
+ {newFileParent === node.path && node.type === "dir" && (
1010
+ <div
1011
+ className="flex items-center gap-1.5 px-2 py-1"
1012
+ style={{ paddingLeft: `${(depth + 1) * 14 + 8}px` }}
1013
+ >
1014
+ <span className="w-3.5 shrink-0" />
1015
+ <FileText className="h-4 w-4 shrink-0 text-accent" />
1016
+ <input
1017
+ autoFocus
1018
+ className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
1019
+ placeholder="filename (default .md)"
1020
+ value={newFileName}
1021
+ onChange={(e) => setNewFileName(e.target.value)}
1022
+ onKeyDown={(e) => {
1023
+ if (e.key === "Enter") handleCreateFile();
1024
+ if (e.key === "Escape") {
1025
+ setNewFileParent(null);
1026
+ setNewFileName("");
1027
+ }
1028
+ }}
1029
+ />
1030
+ {fileCreateError && (
1031
+ <span className="text-xs text-destructive">{fileCreateError}</span>
1032
+ )}
1033
+ <Button
1034
+ size="sm"
1035
+ variant="ghost"
1036
+ className="h-6 w-6 p-0"
1037
+ onClick={handleCreateFile}
1038
+ >
1039
+ <Check className="h-3 w-3" />
1040
+ </Button>
1041
+ <Button
1042
+ size="sm"
1043
+ variant="ghost"
1044
+ className="h-6 w-6 p-0"
1045
+ onClick={() => {
1046
+ setNewFileParent(null);
1047
+ setNewFileName("");
1048
+ }}
1049
+ >
1050
+ <X className="h-3 w-3" />
1051
+ </Button>
1052
+ </div>
1053
+ )}
1054
+
822
1055
  {(node.type === "dir" || node.type === "app") &&
823
1056
  node.expanded &&
824
1057
  node.children &&
@@ -859,12 +1092,76 @@ export default function Page() {
859
1092
  {/* Tree sidebar */}
860
1093
  {!sidebarCollapsed && (
861
1094
  <Card className="flex flex-col w-72 shrink-0 overflow-hidden rounded-none border-r border-l-0 border-t-0 border-b-0">
862
- <div className="flex items-center justify-between px-3 py-2 border-b bg-muted shrink-0">
863
- <div className="flex items-center gap-1.5">
864
- <img src="/logo.svg" alt="Wiki Viewer" className="h-5 w-5" />
865
- <span className="text-xs font-semibold tracking-tight">Wiki Viewer</span>
1095
+ {/* Row 1: brand + collapse */}
1096
+ <div className="flex items-center justify-between gap-2 px-3 py-2 border-b bg-muted shrink-0">
1097
+ <div className="flex min-w-0 items-center gap-1.5">
1098
+ <img src="/logo.svg" alt="Wiki Viewer" className="h-5 w-5 shrink-0" />
1099
+ <span className="truncate text-xs font-semibold tracking-tight">
1100
+ Wiki Viewer
1101
+ </span>
866
1102
  </div>
867
- <div className="flex items-center gap-1">
1103
+ <Button
1104
+ size="sm"
1105
+ variant="ghost"
1106
+ className="h-7 w-7 p-0 shrink-0"
1107
+ title="Collapse sidebar"
1108
+ onClick={() => setSidebarCollapsed(true)}
1109
+ >
1110
+ <PanelLeftClose className="h-3.5 w-3.5" />
1111
+ </Button>
1112
+ </div>
1113
+
1114
+ {/* Row 2: actions toolbar */}
1115
+ <div className="flex items-center justify-between gap-1 px-3 py-1.5 border-b bg-muted/50 shrink-0">
1116
+ <DropdownMenu>
1117
+ <DropdownMenuTrigger asChild>
1118
+ <Button
1119
+ size="sm"
1120
+ variant="ghost"
1121
+ className="h-7 gap-1 px-2 text-xs"
1122
+ disabled={uploading}
1123
+ >
1124
+ {uploading ? (
1125
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
1126
+ ) : (
1127
+ <Plus className="h-3.5 w-3.5" />
1128
+ )}
1129
+ New
1130
+ <ChevronDown className="h-3 w-3 opacity-60" />
1131
+ </Button>
1132
+ </DropdownMenuTrigger>
1133
+ <DropdownMenuContent align="start" className="w-44">
1134
+ <DropdownMenuItem
1135
+ onClick={() => {
1136
+ setNewFileParent("");
1137
+ setNewFileName("");
1138
+ setFileCreateError(null);
1139
+ }}
1140
+ >
1141
+ <FilePlus className="mr-2 h-3.5 w-3.5" />
1142
+ New file
1143
+ </DropdownMenuItem>
1144
+ <DropdownMenuItem
1145
+ onClick={() => {
1146
+ setNewFolderParent("");
1147
+ setNewFolderName("");
1148
+ setFolderError(null);
1149
+ }}
1150
+ >
1151
+ <FolderPlus className="mr-2 h-3.5 w-3.5" />
1152
+ New folder
1153
+ </DropdownMenuItem>
1154
+ <DropdownMenuItem
1155
+ onClick={() => triggerUpload("")}
1156
+ disabled={uploading}
1157
+ >
1158
+ <Upload className="mr-2 h-3.5 w-3.5" />
1159
+ Upload
1160
+ </DropdownMenuItem>
1161
+ </DropdownMenuContent>
1162
+ </DropdownMenu>
1163
+
1164
+ <div className="flex items-center gap-0.5">
868
1165
  <Button
869
1166
  size="sm"
870
1167
  variant="ghost"
@@ -879,42 +1176,24 @@ export default function Page() {
879
1176
  <RefreshCw className="h-3.5 w-3.5" />
880
1177
  )}
881
1178
  </Button>
1179
+ <ThemeToggle />
882
1180
  <Button
883
1181
  size="sm"
884
1182
  variant="ghost"
885
1183
  className="h-7 w-7 p-0"
886
- title="New root folder"
887
- onClick={() => {
888
- setNewFolderParent("");
889
- setNewFolderName("");
890
- setFolderError(null);
891
- }}
892
- >
893
- <FolderPlus className="h-3.5 w-3.5" />
894
- </Button>
895
- <Button
896
- size="sm"
897
- variant="ghost"
898
- className="h-7 w-7 p-0"
899
- title="Upload to root"
900
- onClick={() => triggerUpload("")}
901
- disabled={uploading}
1184
+ title="AI Agent panel"
1185
+ onClick={() => useAIPanelStore.getState().toggle()}
902
1186
  >
903
- {uploading ? (
904
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
905
- ) : (
906
- <Upload className="h-3.5 w-3.5" />
907
- )}
1187
+ <Bot className="h-3.5 w-3.5" />
908
1188
  </Button>
909
- <ThemeToggle />
910
1189
  <Button
911
1190
  size="sm"
912
1191
  variant="ghost"
913
1192
  className="h-7 w-7 p-0"
914
- title="Collapse sidebar"
915
- onClick={() => setSidebarCollapsed(true)}
1193
+ title="Settings"
1194
+ onClick={() => setSettingsOpen(true)}
916
1195
  >
917
- <PanelLeftClose className="h-3.5 w-3.5" />
1196
+ <Settings className="h-3.5 w-3.5" />
918
1197
  </Button>
919
1198
  </div>
920
1199
  </div>
@@ -967,6 +1246,48 @@ export default function Page() {
967
1246
  </div>
968
1247
  )}
969
1248
 
1249
+ {newFileParent === "" && (
1250
+ <div className="flex items-center gap-1.5 px-2 py-1 border-b shrink-0">
1251
+ <FileText className="h-4 w-4 shrink-0 text-accent" />
1252
+ <input
1253
+ autoFocus
1254
+ className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
1255
+ placeholder="filename (default .md)"
1256
+ value={newFileName}
1257
+ onChange={(e) => setNewFileName(e.target.value)}
1258
+ onKeyDown={(e) => {
1259
+ if (e.key === "Enter") handleCreateFile();
1260
+ if (e.key === "Escape") {
1261
+ setNewFileParent(null);
1262
+ setNewFileName("");
1263
+ }
1264
+ }}
1265
+ />
1266
+ {fileCreateError && (
1267
+ <span className="text-xs text-destructive">{fileCreateError}</span>
1268
+ )}
1269
+ <Button
1270
+ size="sm"
1271
+ variant="ghost"
1272
+ className="h-6 w-6 p-0"
1273
+ onClick={handleCreateFile}
1274
+ >
1275
+ <Check className="h-3 w-3" />
1276
+ </Button>
1277
+ <Button
1278
+ size="sm"
1279
+ variant="ghost"
1280
+ className="h-6 w-6 p-0"
1281
+ onClick={() => {
1282
+ setNewFileParent(null);
1283
+ setNewFileName("");
1284
+ }}
1285
+ >
1286
+ <X className="h-3 w-3" />
1287
+ </Button>
1288
+ </div>
1289
+ )}
1290
+
970
1291
  <div
971
1292
  className={cn(
972
1293
  "flex-1 overflow-auto py-1",
@@ -1077,6 +1398,23 @@ export default function Page() {
1077
1398
  </span>
1078
1399
  </div>
1079
1400
  <div className="flex items-center gap-1 shrink-0">
1401
+ {openFileViewerKind === "html" &&
1402
+ !editing &&
1403
+ fileContent !== null && (
1404
+ <Button
1405
+ size="sm"
1406
+ variant="ghost"
1407
+ className="h-7 w-7 p-0"
1408
+ title="Edit source"
1409
+ onClick={() => {
1410
+ setEditing(true);
1411
+ setEditContent(fileContent);
1412
+ setSaveError(null);
1413
+ }}
1414
+ >
1415
+ <Pencil className="h-3.5 w-3.5" />
1416
+ </Button>
1417
+ )}
1080
1418
  <Button
1081
1419
  size="sm"
1082
1420
  variant="ghost"
@@ -1099,18 +1437,60 @@ export default function Page() {
1099
1437
  size="sm"
1100
1438
  variant="ghost"
1101
1439
  className="h-7 w-7 p-0"
1102
- onClick={() => setOpenFile(null)}
1440
+ onClick={() => {
1441
+ setOpenFile(null);
1442
+ setEditing(false);
1443
+ }}
1103
1444
  >
1104
1445
  <X className="h-3.5 w-3.5" />
1105
1446
  </Button>
1106
1447
  </div>
1107
1448
  </div>
1108
- <WebsiteViewer
1109
- key={appKey}
1110
- path={openFile.path}
1111
- title={openFile.name}
1112
- src={websiteSrc}
1113
- />
1449
+ {editing && openFileViewerKind === "html" ? (
1450
+ <div className="flex-1 flex flex-col overflow-hidden min-h-0">
1451
+ <textarea
1452
+ value={editContent}
1453
+ onChange={(e) => setEditContent(e.target.value)}
1454
+ spellCheck={false}
1455
+ className="flex-1 w-full min-h-0 resize-none bg-background text-foreground px-4 py-3 font-mono text-[13px] leading-relaxed outline-none border-0"
1456
+ />
1457
+ <div className="border-t px-4 py-2 flex items-center justify-end gap-2 bg-muted shrink-0">
1458
+ {saveError && (
1459
+ <span className="text-xs text-destructive mr-auto">
1460
+ {saveError}
1461
+ </span>
1462
+ )}
1463
+ <Button
1464
+ size="sm"
1465
+ variant="ghost"
1466
+ onClick={() => {
1467
+ setEditing(false);
1468
+ setSaveError(null);
1469
+ }}
1470
+ >
1471
+ Cancel
1472
+ </Button>
1473
+ <Button
1474
+ size="sm"
1475
+ className="gap-1"
1476
+ onClick={handleSave}
1477
+ disabled={saving}
1478
+ >
1479
+ {saving && (
1480
+ <Loader2 className="h-3 w-3 animate-spin" />
1481
+ )}
1482
+ Save
1483
+ </Button>
1484
+ </div>
1485
+ </div>
1486
+ ) : (
1487
+ <WebsiteViewer
1488
+ key={appKey}
1489
+ path={openFile.path}
1490
+ title={openFile.name}
1491
+ src={websiteSrc}
1492
+ />
1493
+ )}
1114
1494
  </div>
1115
1495
  );
1116
1496
  })()
@@ -1144,9 +1524,11 @@ export default function Page() {
1144
1524
  setEditing(true);
1145
1525
  setEditContent(fileContent);
1146
1526
  setSaveError(null);
1147
- void useEditorStore
1148
- .getState()
1149
- .loadPage(openFile.path);
1527
+ if (isMarkdown(openFile.name)) {
1528
+ void useEditorStore
1529
+ .getState()
1530
+ .loadPage(openFile.path);
1531
+ }
1150
1532
  }}
1151
1533
  >
1152
1534
  <Pencil className="h-3.5 w-3.5" />
@@ -1170,6 +1552,7 @@ export default function Page() {
1170
1552
  )}
1171
1553
  </Button>
1172
1554
  )}
1555
+ {widthAwareViewer && <ViewWidthToggle />}
1173
1556
  <Button
1174
1557
  size="sm"
1175
1558
  variant="ghost"
@@ -1187,10 +1570,20 @@ export default function Page() {
1187
1570
 
1188
1571
  {editing ? (
1189
1572
  <div className="flex-1 flex flex-col overflow-hidden min-h-0">
1190
- <KBEditor />
1573
+ {isMarkdown(openFile.name) ? (
1574
+ <KBEditor />
1575
+ ) : (
1576
+ <textarea
1577
+ value={editContent}
1578
+ onChange={(e) => setEditContent(e.target.value)}
1579
+ spellCheck={false}
1580
+ className="flex-1 w-full min-h-0 resize-none bg-background text-foreground px-4 py-3 font-mono text-[13px] leading-relaxed outline-none border-0"
1581
+ />
1582
+ )}
1191
1583
  </div>
1192
1584
  ) : (
1193
1585
  <div className="flex-1 overflow-auto p-4 min-h-0">
1586
+ <div className={cn("mx-auto w-full", contentWidthClass)}>
1194
1587
  {fileLoading ? (
1195
1588
  <div className="flex justify-center py-8">
1196
1589
  <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
@@ -1248,176 +1641,24 @@ export default function Page() {
1248
1641
  <FrontmatterHeader
1249
1642
  data={data as Record<string, never>}
1250
1643
  />
1251
- <ReactMarkdown
1252
- remarkPlugins={[remarkGfm, remarkWikilinks]}
1253
- components={{
1254
- h1: ({ children }) => (
1255
- <h1 className="text-2xl font-normal mt-6 mb-3 pb-1 border-b">
1256
- {children}
1257
- </h1>
1258
- ),
1259
- h2: ({ children }) => (
1260
- <h2 className="text-xl font-normal mt-5 mb-2 pb-1 border-b">
1261
- {children}
1262
- </h2>
1263
- ),
1264
- h3: ({ children }) => (
1265
- <h3 className="text-lg font-normal mt-4 mb-2">
1266
- {children}
1267
- </h3>
1268
- ),
1269
- h4: ({ children }) => (
1270
- <h4 className="text-base font-normal mt-3 mb-1">
1271
- {children}
1272
- </h4>
1273
- ),
1274
- p: ({ children }) => (
1275
- <p className="text-sm leading-relaxed mb-3">
1276
- {children}
1277
- </p>
1278
- ),
1279
- ul: ({ children }) => (
1280
- <ul className="list-disc pl-5 mb-3 space-y-1 text-sm">
1281
- {children}
1282
- </ul>
1283
- ),
1284
- ol: ({ children }) => (
1285
- <ol className="list-decimal pl-5 mb-3 space-y-1 text-sm">
1286
- {children}
1287
- </ol>
1288
- ),
1289
- li: ({ children }) => (
1290
- <li className="leading-relaxed">
1291
- {children}
1292
- </li>
1293
- ),
1294
- blockquote: ({ children }) => (
1295
- <blockquote className="border-l-4 border-muted-foreground/30 pl-4 italic text-muted-foreground my-3 text-sm">
1296
- {children}
1297
- </blockquote>
1298
- ),
1299
- code: ({ className, children, ...props }) => {
1300
- const isBlock =
1301
- className?.includes("language-");
1302
- return isBlock ? (
1303
- <code
1304
- className={`block bg-muted rounded-sm px-3 py-2 text-xs font-mono overflow-x-auto my-3 ${className ?? ""}`}
1305
- {...props}
1306
- >
1307
- {children}
1308
- </code>
1309
- ) : (
1310
- <code
1311
- className="bg-muted rounded-sm px-1 py-0.5 text-xs font-mono"
1312
- {...props}
1313
- >
1314
- {children}
1315
- </code>
1316
- );
1317
- },
1318
- pre: ({ children }) => (
1319
- <pre className="my-3 overflow-x-auto">
1320
- {children}
1321
- </pre>
1322
- ),
1323
- a: ({ href, children, ...rest }) => {
1324
- const props = rest as Record<
1325
- string,
1326
- unknown
1327
- >;
1328
- if (props["data-wiki-link"] === "true") {
1329
- const slug =
1330
- (props["data-slug"] as string) ?? "";
1331
- const anchor = props["data-anchor"] as
1332
- | string
1333
- | undefined;
1334
- const broken =
1335
- slug &&
1336
- !useWikiSlugsStore
1337
- .getState()
1338
- .has(slug);
1339
- return (
1340
- <a
1341
- href={href}
1342
- className="wiki-link"
1343
- data-wiki-link="true"
1344
- data-slug={slug}
1345
- data-anchor={anchor}
1346
- data-broken={
1347
- broken ? "true" : undefined
1348
- }
1349
- onClick={(e) => {
1350
- e.preventDefault();
1351
- if (!slug) return;
1352
- const dir = useWikiSlugsStore
1353
- .getState()
1354
- .getDir(slug);
1355
- if (!dir) return;
1356
- const targetPath =
1357
- dir === "root"
1358
- ? `${slug}.md`
1359
- : `${dir}/${slug}.md`;
1360
- void openViewer({
1361
- path: targetPath,
1362
- name: `${slug}.md`,
1363
- type: "file",
1364
- } as TreeNode);
1365
- if (anchor) {
1366
- setTimeout(() => {
1367
- document
1368
- .getElementById(anchor)
1369
- ?.scrollIntoView({
1370
- behavior: "smooth",
1371
- });
1372
- }, 200);
1373
- }
1374
- }}
1375
- >
1376
- {children}
1377
- </a>
1378
- );
1379
- }
1380
- return (
1381
- <a
1382
- href={href}
1383
- className="text-primary underline hover:no-underline"
1384
- target="_blank"
1385
- rel="noreferrer"
1386
- >
1387
- {children}
1388
- </a>
1389
- );
1390
- },
1391
- strong: ({ children }) => (
1392
- <strong className="font-normal">
1393
- {children}
1394
- </strong>
1395
- ),
1396
- em: ({ children }) => (
1397
- <em className="italic">{children}</em>
1398
- ),
1399
- hr: () => <hr className="my-4 border-border" />,
1400
- table: ({ children }) => (
1401
- <div className="overflow-x-auto my-3">
1402
- <table className="w-full text-sm border-collapse">
1403
- {children}
1404
- </table>
1405
- </div>
1406
- ),
1407
- th: ({ children }) => (
1408
- <th className="border border-border px-3 py-1.5 bg-muted font-normal text-left">
1409
- {children}
1410
- </th>
1411
- ),
1412
- td: ({ children }) => (
1413
- <td className="border border-border px-3 py-1.5">
1414
- {children}
1415
- </td>
1416
- ),
1644
+ <MarkdownPreview
1645
+ markdown={body}
1646
+ pagePath={openFile.path}
1647
+ onNavigate={(targetPath, anchor) => {
1648
+ void openViewer({
1649
+ path: targetPath,
1650
+ name: targetPath.split('/').pop() ?? targetPath,
1651
+ type: 'file',
1652
+ } as TreeNode);
1653
+ if (anchor) {
1654
+ setTimeout(() => {
1655
+ document
1656
+ .getElementById(anchor)
1657
+ ?.scrollIntoView({ behavior: 'smooth' });
1658
+ }, 200);
1659
+ }
1417
1660
  }}
1418
- >
1419
- {body}
1420
- </ReactMarkdown>
1661
+ />
1421
1662
  </>
1422
1663
  );
1423
1664
  })()
@@ -1435,6 +1676,7 @@ export default function Page() {
1435
1676
  Preview not available for this file type.
1436
1677
  </p>
1437
1678
  )}
1679
+ </div>
1438
1680
  </div>
1439
1681
  )}
1440
1682
 
@@ -1480,6 +1722,8 @@ export default function Page() {
1480
1722
  )}
1481
1723
  </div>
1482
1724
 
1725
+ <AuthSettingsSheet open={settingsOpen} onOpenChange={setSettingsOpen} />
1726
+ <AIPanel currentPath={openFile?.path} />
1483
1727
  <input
1484
1728
  ref={fileInputRef}
1485
1729
  type="file"