wiki-viewer 1.0.0 → 1.1.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 (128) hide show
  1. package/.next/standalone/README.md +365 -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 +431 -33
  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 +365 -99
  127. package/bin/wiki-viewer.js +431 -33
  128. package/package.json +19 -2
@@ -0,0 +1,217 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, type FormEvent } from "react";
4
+ import { authClient } from "@/lib/auth/client";
5
+
6
+ export default function SignInPage() {
7
+ const [mode, setMode] = useState<"signin" | "signup">("signin");
8
+ const [email, setEmail] = useState("");
9
+ const [password, setPassword] = useState("");
10
+ const [name, setName] = useState("");
11
+ const [error, setError] = useState<string | null>(null);
12
+ const [loading, setLoading] = useState(false);
13
+ const [hasGoogle, setHasGoogle] = useState(false);
14
+ const [callbackURL, setCallbackURL] = useState("/");
15
+
16
+ useEffect(() => {
17
+ // Detect Google provider availability via the providers endpoint
18
+ fetch("/api/auth/list-providers", { credentials: "include" })
19
+ .then((r) => r.json())
20
+ .then((data: unknown) => {
21
+ if (
22
+ data &&
23
+ typeof data === "object" &&
24
+ "google" in data
25
+ ) {
26
+ setHasGoogle(true);
27
+ }
28
+ })
29
+ .catch(() => {});
30
+
31
+ // Honour ?next= redirect param
32
+ const params = new URLSearchParams(window.location.search);
33
+ const next = params.get("next");
34
+ if (next && next.startsWith("/")) setCallbackURL(next);
35
+ }, []);
36
+
37
+ async function handleSubmit(e: FormEvent) {
38
+ e.preventDefault();
39
+ setError(null);
40
+ setLoading(true);
41
+ try {
42
+ if (mode === "signup") {
43
+ const res = await authClient.signUp.email({
44
+ email,
45
+ password,
46
+ name,
47
+ callbackURL,
48
+ });
49
+ if (res.error) {
50
+ setError(res.error.message ?? "Sign-up failed");
51
+ } else {
52
+ window.location.href = callbackURL;
53
+ }
54
+ } else {
55
+ const res = await authClient.signIn.email({
56
+ email,
57
+ password,
58
+ callbackURL,
59
+ });
60
+ if (res.error) {
61
+ const status = res.error.status;
62
+ if (status === 429) {
63
+ setError("Too many sign-in attempts. Wait a minute and try again.");
64
+ } else {
65
+ setError(res.error.message ?? "Sign-in failed");
66
+ }
67
+ } else {
68
+ window.location.href = callbackURL;
69
+ }
70
+ }
71
+ } catch (err: unknown) {
72
+ setError(err instanceof Error ? err.message : "Unexpected error");
73
+ } finally {
74
+ setLoading(false);
75
+ }
76
+ }
77
+
78
+ async function handleGoogle() {
79
+ setError(null);
80
+ setLoading(true);
81
+ try {
82
+ await authClient.signIn.social({ provider: "google", callbackURL });
83
+ } catch (err: unknown) {
84
+ setError(err instanceof Error ? err.message : "Google sign-in failed");
85
+ setLoading(false);
86
+ }
87
+ }
88
+
89
+ return (
90
+ <div className="min-h-screen flex items-center justify-center bg-background">
91
+ <div className="w-full max-w-sm rounded-lg border border-border bg-card p-8 shadow-sm">
92
+ <h1 className="mb-6 text-xl font-semibold text-foreground">Sign in to wiki-viewer</h1>
93
+
94
+ {hasGoogle && (
95
+ <>
96
+ <button
97
+ type="button"
98
+ onClick={handleGoogle}
99
+ disabled={loading}
100
+ className="flex w-full items-center justify-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50"
101
+ >
102
+ <svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
103
+ <path
104
+ fill="#4285F4"
105
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
106
+ />
107
+ <path
108
+ fill="#34A853"
109
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
110
+ />
111
+ <path
112
+ fill="#FBBC05"
113
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
114
+ />
115
+ <path
116
+ fill="#EA4335"
117
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
118
+ />
119
+ </svg>
120
+ Continue with Google
121
+ </button>
122
+ <div className="my-4 flex items-center gap-3">
123
+ <hr className="flex-1 border-border" />
124
+ <span className="text-xs text-muted-foreground">or</span>
125
+ <hr className="flex-1 border-border" />
126
+ </div>
127
+ </>
128
+ )}
129
+
130
+ <form onSubmit={handleSubmit} className="space-y-4">
131
+ {mode === "signup" && (
132
+ <div>
133
+ <label className="mb-1 block text-sm font-medium text-foreground" htmlFor="name">
134
+ Name
135
+ </label>
136
+ <input
137
+ id="name"
138
+ type="text"
139
+ autoComplete="name"
140
+ value={name}
141
+ onChange={(e) => setName(e.target.value)}
142
+ required
143
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
144
+ />
145
+ </div>
146
+ )}
147
+ <div>
148
+ <label className="mb-1 block text-sm font-medium text-foreground" htmlFor="email">
149
+ Email
150
+ </label>
151
+ <input
152
+ id="email"
153
+ type="email"
154
+ autoComplete="email"
155
+ value={email}
156
+ onChange={(e) => setEmail(e.target.value)}
157
+ required
158
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
159
+ />
160
+ </div>
161
+ <div>
162
+ <label
163
+ className="mb-1 block text-sm font-medium text-foreground"
164
+ htmlFor="password"
165
+ >
166
+ Password
167
+ </label>
168
+ <input
169
+ id="password"
170
+ type="password"
171
+ autoComplete={mode === "signup" ? "new-password" : "current-password"}
172
+ value={password}
173
+ onChange={(e) => setPassword(e.target.value)}
174
+ required
175
+ minLength={8}
176
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
177
+ />
178
+ </div>
179
+
180
+ {error && (
181
+ <p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
182
+ {error}
183
+ </p>
184
+ )}
185
+
186
+ <button
187
+ type="submit"
188
+ disabled={loading}
189
+ className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
190
+ >
191
+ {loading
192
+ ? mode === "signup"
193
+ ? "Creating account..."
194
+ : "Signing in..."
195
+ : mode === "signup"
196
+ ? "Create account"
197
+ : "Sign in"}
198
+ </button>
199
+ </form>
200
+
201
+ <p className="mt-4 text-center text-sm text-muted-foreground">
202
+ {mode === "signin" ? "No account?" : "Already have an account?"}{" "}
203
+ <button
204
+ type="button"
205
+ onClick={() => {
206
+ setMode(mode === "signin" ? "signup" : "signin");
207
+ setError(null);
208
+ }}
209
+ className="font-medium text-foreground underline-offset-4 hover:underline"
210
+ >
211
+ {mode === "signin" ? "Sign up" : "Sign in"}
212
+ </button>
213
+ </p>
214
+ </div>
215
+ </div>
216
+ );
217
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import type { ActivityEvent } from "@/lib/proof/activity-shared";
4
+
5
+ function timeAgo(iso: string): string {
6
+ const diff = Math.max(0, Date.now() - new Date(iso).getTime());
7
+ const s = Math.floor(diff / 1000);
8
+ if (s < 60) return `${s}s ago`;
9
+ const m = Math.floor(s / 60);
10
+ if (m < 60) return `${m}m ago`;
11
+ const h = Math.floor(m / 60);
12
+ if (h < 24) return `${h}h ago`;
13
+ return `${Math.floor(h / 24)}d ago`;
14
+ }
15
+
16
+ function verbFor(type: string): string {
17
+ const map: Record<string, string> = {
18
+ "block.replace": "replaced",
19
+ "block.insertAfter": "inserted after",
20
+ "block.insertBefore": "inserted before",
21
+ "block.delete": "deleted",
22
+ "block.append": "appended",
23
+ "block.prepend": "prepended",
24
+ "comment.add": "commented on",
25
+ "comment.reply": "replied to comment",
26
+ "comment.resolve": "resolved comment",
27
+ "comment.reopen": "reopened comment",
28
+ "suggestion.add": "suggested",
29
+ "suggestion.accept": "accepted suggestion",
30
+ "suggestion.reject": "rejected suggestion",
31
+ };
32
+ return map[type] ?? type;
33
+ }
34
+
35
+ export function ActivityRow({ event }: { event: ActivityEvent }) {
36
+ const filename = event.path.split("/").pop() ?? event.path;
37
+ const ref = typeof event["ref"] === "string" ? (event["ref"] as string) : "";
38
+
39
+ return (
40
+ <div className="flex flex-col gap-0.5 py-1.5 border-b border-border/50 last:border-0">
41
+ <div className="flex items-baseline justify-between gap-2">
42
+ <span
43
+ className="text-xs font-mono text-foreground/80 truncate"
44
+ title={event.path}
45
+ >
46
+ {filename}
47
+ </span>
48
+ <span className="text-[10px] text-muted-foreground shrink-0">
49
+ {timeAgo(event.at)}
50
+ </span>
51
+ </div>
52
+ <p className="text-[11px] text-muted-foreground truncate">
53
+ <span className="text-foreground/60">{event.by}</span>{" "}
54
+ {verbFor(event.type)}
55
+ {ref && (
56
+ <>
57
+ {" "}
58
+ <span className="font-mono text-[10px] text-muted-foreground/60">{ref}</span>
59
+ </>
60
+ )}
61
+ </p>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,322 @@
1
+ "use client";
2
+
3
+ import { useEffect, useCallback, useRef, useState } from "react";
4
+ import { X, Copy, Check, Bot, Wifi, Download, ExternalLink } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { useAIPanelStore } from "@/stores/ai-panel-store";
7
+ import { TokenSection } from "./token-section";
8
+ import { ActivityRow } from "./activity-row";
9
+ import { authClient } from "@/lib/auth/client";
10
+ const SKILL_CLI = "npx skills add anh-chu/wiki-viewer/agents/wiki-viewer-skill";
11
+
12
+ function useCopyCurl(currentPath: string | null) {
13
+ const [copied, setCopied] = useState(false);
14
+ const copy = useCallback(async () => {
15
+ const origin =
16
+ typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
17
+ const filePath = currentPath ?? "<path/to/file.md>";
18
+ // Registration-flow curl trace
19
+ const curl = [
20
+ `# 1. Register`,
21
+ `REG=$(curl -s -X POST -H 'Content-Type: application/json' \\`,
22
+ ` -d '{"id":"ai:claude","displayName":"Claude"}' \\`,
23
+ ` ${origin}/api/agent/register)`,
24
+ `REG_ID=$(echo $REG | jq -r .registrationId)`,
25
+ ``,
26
+ `# 2. Approve in AI Panel, then poll:`,
27
+ `curl -s ${origin}/api/agent/register/$REG_ID`,
28
+ `# → {"status":"approved","token":"..."}`,
29
+ `TOKEN=<paste token here>`,
30
+ ``,
31
+ `# 3. Read snapshot`,
32
+ `curl -s -H "Authorization: Bearer $TOKEN" \\`,
33
+ ` -H "X-Agent-Id: ai:claude" \\`,
34
+ ` ${origin}/api/agent/files/${filePath}`,
35
+ ].join("\n");
36
+ await navigator.clipboard.writeText(curl);
37
+ setCopied(true);
38
+ setTimeout(() => setCopied(false), 1500);
39
+ }, [currentPath]);
40
+ return { copied, copy };
41
+ }
42
+
43
+ function useCopyButton(getText: () => string): { copied: boolean; copy: () => Promise<void> } {
44
+ const [copied, setCopied] = useState(false);
45
+ const copy = useCallback(async () => {
46
+ await navigator.clipboard.writeText(getText());
47
+ setCopied(true);
48
+ setTimeout(() => setCopied(false), 1500);
49
+ }, [getText]);
50
+ return { copied, copy };
51
+ }
52
+
53
+ export function AIPanel({ currentPath }: { currentPath?: string | null }) {
54
+ const isOpen = useAIPanelStore((s) => s.isOpen);
55
+ const close = useAIPanelStore((s) => s.close);
56
+ const activity = useAIPanelStore((s) => s.activity);
57
+ const connections = useAIPanelStore((s) => s.connections);
58
+ const rateLimit = useAIPanelStore((s) => s.rateLimit);
59
+ const loadActivity = useAIPanelStore((s) => s.loadActivity);
60
+ const { copied, copy } = useCopyCurl(currentPath ?? null);
61
+ const [bootstrapPrompt, setBootstrapPrompt] = useState<string>("");
62
+ const panelRef = useRef<HTMLElement>(null);
63
+
64
+ const origin =
65
+ typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
66
+ const getSkillCli = useCallback(() => SKILL_CLI, []);
67
+ const getBootstrapPrompt = useCallback(
68
+ () => bootstrapPrompt.replace(/\$WIKI_URL/g, origin),
69
+ [bootstrapPrompt, origin]
70
+ );
71
+ const skillCli = useCopyButton(getSkillCli);
72
+ const bootstrapCopy = useCopyButton(getBootstrapPrompt);
73
+
74
+ // Keyboard: Esc closes
75
+ useEffect(() => {
76
+ if (!isOpen) return;
77
+ function onKey(e: KeyboardEvent) {
78
+ if (e.key === "Escape") close();
79
+ }
80
+ document.addEventListener("keydown", onKey);
81
+ return () => document.removeEventListener("keydown", onKey);
82
+ }, [isOpen, close]);
83
+
84
+ // Focus trap: focus panel when open
85
+ useEffect(() => {
86
+ if (isOpen) panelRef.current?.focus();
87
+ }, [isOpen]);
88
+
89
+ // Poll while open
90
+ useEffect(() => {
91
+ if (!isOpen) return;
92
+ void loadActivity();
93
+ const id = setInterval(() => void loadActivity(), 10_000);
94
+ return () => clearInterval(id);
95
+ }, [isOpen, loadActivity]);
96
+
97
+ // Fetch install JSON once on open for live bootstrap prompt
98
+ useEffect(() => {
99
+ if (!isOpen) return;
100
+ void fetch("/api/agents/install")
101
+ .then((r) => r.json())
102
+ .then((d: { bootstrapPrompt?: string }) => {
103
+ if (d.bootstrapPrompt) setBootstrapPrompt(d.bootstrapPrompt);
104
+ })
105
+ .catch(() => undefined);
106
+ }, [isOpen]);
107
+
108
+ if (!isOpen) return null;
109
+
110
+ return (
111
+ <>
112
+ {/* Backdrop */}
113
+ <div
114
+ className="fixed inset-0 z-40 bg-black/20 backdrop-blur-[1px]"
115
+ onClick={close}
116
+ aria-hidden
117
+ />
118
+
119
+ {/* Panel */}
120
+ <aside
121
+ ref={panelRef}
122
+ tabIndex={-1}
123
+ role="dialog"
124
+ aria-modal="true"
125
+ aria-label="AI Agent Panel"
126
+ className="fixed right-0 top-0 bottom-0 z-50 flex w-80 flex-col border-l border-border bg-background shadow-xl outline-none"
127
+ >
128
+ {/* Header */}
129
+ <div className="flex items-center justify-between border-b border-border px-4 py-3 shrink-0">
130
+ <div className="flex items-center gap-2">
131
+ <Bot className="h-4 w-4 text-muted-foreground" />
132
+ <span className="text-sm font-semibold">AI Agent</span>
133
+ </div>
134
+ <Button
135
+ size="sm"
136
+ variant="ghost"
137
+ className="h-7 w-7 p-0"
138
+ onClick={close}
139
+ title="Close panel (Esc)"
140
+ >
141
+ <X className="h-3.5 w-3.5" />
142
+ </Button>
143
+ </div>
144
+
145
+ {/* Scrollable body */}
146
+ <div className="flex-1 overflow-y-auto space-y-5 px-4 py-4">
147
+ <SignedInUser />
148
+
149
+ {/* Bridge endpoint */}
150
+ <section className="space-y-2">
151
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
152
+ Bridge Endpoint
153
+ </h3>
154
+ <div className="rounded-md border border-border bg-muted/40 p-3 space-y-2">
155
+ <div className="flex items-center gap-2">
156
+ <code className="flex-1 text-xs font-mono text-foreground/80 truncate">
157
+ {origin}
158
+ </code>
159
+ <Button
160
+ size="sm"
161
+ variant="ghost"
162
+ className="h-6 w-6 p-0 shrink-0"
163
+ title="Copy curl example"
164
+ onClick={() => void copy()}
165
+ >
166
+ {copied ? (
167
+ <Check className="h-3.5 w-3.5 text-green-500" />
168
+ ) : (
169
+ <Copy className="h-3.5 w-3.5" />
170
+ )}
171
+ </Button>
172
+ </div>
173
+ <p className="text-[10px] text-muted-foreground/70">
174
+ Copy icon copies a registration-flow <code className="bg-muted px-0.5 rounded">curl</code> trace for the current file.
175
+ </p>
176
+ </div>
177
+ </section>
178
+
179
+ {/* Install for AI agents */}
180
+ <section className="space-y-2">
181
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
182
+ Install for AI Agents
183
+ </h3>
184
+ <div className="rounded-md border border-border bg-muted/40 p-3 space-y-3">
185
+ <div className="space-y-1">
186
+ <p className="text-[10px] text-muted-foreground/70">Agent Skills standard:</p>
187
+ <div className="flex items-center gap-2">
188
+ <code className="flex-1 text-[10px] font-mono text-foreground/80 truncate bg-muted rounded px-1 py-0.5">
189
+ {SKILL_CLI}
190
+ </code>
191
+ <Button size="sm" variant="ghost" className="h-6 w-6 p-0 shrink-0" title="Copy CLI command" onClick={() => void skillCli.copy()}>
192
+ {skillCli.copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
193
+ </Button>
194
+ </div>
195
+ </div>
196
+ <div className="space-y-1">
197
+ <p className="text-[10px] text-muted-foreground/70">Any agent (paste into chat):</p>
198
+ <Button size="sm" variant="outline" className="h-7 w-full text-xs gap-1.5" onClick={() => void bootstrapCopy.copy()} disabled={!bootstrapPrompt}>
199
+ {bootstrapCopy.copied
200
+ ? <><Check className="h-3.5 w-3.5 text-green-500" /> Copied!</>
201
+ : <><Copy className="h-3.5 w-3.5" /> Copy bootstrap prompt</>}
202
+ </Button>
203
+ </div>
204
+ <div className="flex flex-wrap gap-x-3 gap-y-1 pt-1">
205
+ <a href="/api/agents/skill.tar.gz" download className="inline-flex items-center gap-1 text-[10px] text-primary hover:underline">
206
+ <Download className="h-3 w-3" /> skill (.tar.gz)
207
+ </a>
208
+ <a href="/api/agents/skill" target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-[10px] text-primary hover:underline">
209
+ <ExternalLink className="h-3 w-3" /> skill markdown
210
+ </a>
211
+ <a href="/api/agents/install" target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-[10px] text-primary hover:underline">
212
+ <ExternalLink className="h-3 w-3" /> install JSON
213
+ </a>
214
+ </div>
215
+ </div>
216
+ </section>
217
+
218
+ {/* Token / Agents */}
219
+ <TokenSection />
220
+
221
+ {/* Active connections */}
222
+ <section className="space-y-2">
223
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
224
+ Active Connections{" "}
225
+ <span className="normal-case font-normal text-muted-foreground/50">
226
+ (last 5 min)
227
+ </span>
228
+ </h3>
229
+ <div className="rounded-md border border-border bg-muted/40 p-3">
230
+ {connections.length === 0 ? (
231
+ <p className="text-xs text-muted-foreground/60 flex items-center gap-1.5">
232
+ <Wifi className="h-3.5 w-3.5" />
233
+ No recent activity
234
+ </p>
235
+ ) : (
236
+ <ul className="space-y-1.5">
237
+ {connections.map((c) => (
238
+ <li key={c.by} className="flex items-center justify-between text-xs">
239
+ <span className="font-mono text-foreground/80 truncate">{c.by}</span>
240
+ <span className="text-muted-foreground shrink-0">
241
+ {c.opCount} op{c.opCount !== 1 ? "s" : ""}
242
+ </span>
243
+ </li>
244
+ ))}
245
+ </ul>
246
+ )}
247
+ </div>
248
+ </section>
249
+
250
+ {/* Recent activity */}
251
+ <section className="space-y-2">
252
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
253
+ Recent Activity
254
+ </h3>
255
+ <div className="rounded-md border border-border bg-muted/40 px-3 py-1 max-h-72 overflow-y-auto">
256
+ {activity.length === 0 ? (
257
+ <p className="text-xs text-muted-foreground/60 py-2">No events recorded</p>
258
+ ) : (
259
+ activity.map((ev) => (
260
+ <ActivityRow key={`${ev.path}-${ev.id}`} event={ev} />
261
+ ))
262
+ )}
263
+ </div>
264
+ </section>
265
+
266
+ {/* Settings */}
267
+ <section className="space-y-2">
268
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
269
+ Settings
270
+ </h3>
271
+ <div className="rounded-md border border-border bg-muted/40 p-3 space-y-1.5">
272
+ <div className="flex items-center justify-between text-xs">
273
+ <span className="text-muted-foreground">Rate limit</span>
274
+ <span className="font-mono">
275
+ {rateLimit !== null ? `${rateLimit} ops/min` : "--"}
276
+ </span>
277
+ </div>
278
+ <p className="text-[10px] text-muted-foreground/60">
279
+ Override with <code className="bg-muted px-0.5 rounded">AGENT_RATE_LIMIT</code> env var.
280
+ </p>
281
+ </div>
282
+ </section>
283
+
284
+ {/* Docs link */}
285
+ <section>
286
+ <a
287
+ href="/docs/agent-collab-plan.md"
288
+ target="_blank"
289
+ rel="noreferrer"
290
+ className="text-xs text-primary underline hover:no-underline"
291
+ >
292
+ Agent collaboration docs →
293
+ </a>
294
+ </section>
295
+ </div>
296
+ </aside>
297
+ </>
298
+ );
299
+ }
300
+
301
+ function SignedInUser() {
302
+ const { data: session } = authClient.useSession();
303
+ if (!session?.user) return null;
304
+ return (
305
+ <div className="flex items-center justify-between rounded-md border border-border bg-muted/40 p-3">
306
+ <div className="text-sm min-w-0">
307
+ <div className="font-medium truncate">{session.user.name}</div>
308
+ <div className="text-xs text-muted-foreground truncate">{session.user.email}</div>
309
+ </div>
310
+ <button
311
+ type="button"
312
+ onClick={async () => {
313
+ await authClient.signOut();
314
+ window.location.href = "/signin";
315
+ }}
316
+ className="ml-2 shrink-0 text-xs px-2 py-1 rounded border border-border hover:bg-accent"
317
+ >
318
+ Sign out
319
+ </button>
320
+ </div>
321
+ );
322
+ }