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.
- package/.next/standalone/README.md +365 -99
- package/.next/standalone/agents/bootstrap-prompt.md +1 -0
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
- package/.next/standalone/bin/wiki-viewer.js +431 -33
- package/.next/standalone/docs/agent-collab-plan.md +1615 -0
- package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
- package/.next/standalone/package.json +19 -2
- package/.next/standalone/pnpm-lock.yaml +1368 -325
- package/.next/standalone/pnpm-workspace.yaml +7 -1
- package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
- package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
- package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
- package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
- package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
- package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
- package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
- package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
- package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
- package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
- package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
- package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
- package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
- package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
- package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
- package/.next/standalone/src/app/api/system/config/route.ts +5 -1
- package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
- package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
- package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/route.ts +10 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
- package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
- package/.next/standalone/src/app/globals.css +70 -0
- package/.next/standalone/src/app/page.tsx +461 -217
- package/.next/standalone/src/app/signin/page.tsx +217 -0
- package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
- package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
- package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
- package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
- package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
- package/.next/standalone/src/components/editor/editor.tsx +513 -10
- package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
- package/.next/standalone/src/components/editor/extensions.ts +2 -0
- package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
- package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
- package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
- package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
- package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
- package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
- package/.next/standalone/src/lib/auth/client.ts +4 -0
- package/.next/standalone/src/lib/auth/csrf.ts +68 -0
- package/.next/standalone/src/lib/auth/server.ts +164 -0
- package/.next/standalone/src/lib/config.ts +4 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
- package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
- package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
- package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
- package/.next/standalone/src/lib/proof/activity.ts +74 -0
- package/.next/standalone/src/lib/proof/auth.ts +132 -0
- package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
- package/.next/standalone/src/lib/proof/blocks.ts +73 -0
- package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
- package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
- package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
- package/.next/standalone/src/lib/proof/glob.ts +51 -0
- package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
- package/.next/standalone/src/lib/proof/mutex.ts +32 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
- package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
- package/.next/standalone/src/lib/proof/pending.ts +97 -0
- package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
- package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/registry.ts +190 -0
- package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
- package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
- package/.next/standalone/src/lib/proof/types.ts +170 -0
- package/.next/standalone/src/lib/proof-config.ts +14 -0
- package/.next/standalone/src/middleware.ts +38 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
- package/.next/standalone/src/stores/editor-store.ts +115 -9
- package/.next/standalone/src/stores/proof-store.ts +123 -0
- package/.next/standalone/src/stores/view-width-store.ts +62 -0
- package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
- package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
- package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
- package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
- package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
- package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
- package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
- package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
- package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
- package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
- package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
- package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
- package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
- package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
- package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
- package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
- package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
- package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
- package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
- package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
- package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
- package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
- package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
- package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
- package/README.md +365 -99
- package/bin/wiki-viewer.js +431 -33
- package/package.json +19 -2
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { CheckCircle, XCircle, Clock, Trash2, RefreshCw } from "lucide-react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
|
|
8
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
interface PendingReg {
|
|
11
|
+
registrationId: string;
|
|
12
|
+
agentId: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
requestedScope: { paths: string[]; ops: string[] };
|
|
15
|
+
requestedAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RegisteredAgent {
|
|
19
|
+
id: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
scope: { paths: string[]; ops: string[] };
|
|
22
|
+
createdAt: string;
|
|
23
|
+
lastSeen: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function relativeTime(iso: string): string {
|
|
27
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
28
|
+
if (diff < 60_000) return "just now";
|
|
29
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
30
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
31
|
+
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Sub-components ────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function ScopeTag({ scope }: { scope: { paths: string[]; ops: string[] } }) {
|
|
37
|
+
return (
|
|
38
|
+
<span className="text-[10px] text-muted-foreground/70 font-mono">
|
|
39
|
+
{scope.paths.join(", ")} · {scope.ops.join(",")}
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function PendingRow({
|
|
45
|
+
reg,
|
|
46
|
+
onApprove,
|
|
47
|
+
onDeny,
|
|
48
|
+
}: {
|
|
49
|
+
reg: PendingReg;
|
|
50
|
+
onApprove: (id: string) => Promise<void>;
|
|
51
|
+
onDeny: (id: string) => Promise<void>;
|
|
52
|
+
}) {
|
|
53
|
+
const [busy, setBusy] = useState(false);
|
|
54
|
+
|
|
55
|
+
async function handle(fn: (id: string) => Promise<void>) {
|
|
56
|
+
setBusy(true);
|
|
57
|
+
try {
|
|
58
|
+
await fn(reg.registrationId);
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<li className="py-1.5 border-b border-border/40 last:border-0">
|
|
66
|
+
<div className="flex items-start justify-between gap-1">
|
|
67
|
+
<div className="min-w-0">
|
|
68
|
+
<p className="text-xs font-mono truncate">{reg.displayName}</p>
|
|
69
|
+
<p className="text-[10px] text-muted-foreground/60 truncate">{reg.agentId}</p>
|
|
70
|
+
<ScopeTag scope={reg.requestedScope} />
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
73
|
+
<Button
|
|
74
|
+
size="sm"
|
|
75
|
+
variant="default"
|
|
76
|
+
className="h-6 px-2 text-[10px]"
|
|
77
|
+
disabled={busy}
|
|
78
|
+
onClick={() => void handle(onApprove)}
|
|
79
|
+
title="Approve"
|
|
80
|
+
>
|
|
81
|
+
<CheckCircle className="h-3 w-3 mr-0.5" />
|
|
82
|
+
Approve
|
|
83
|
+
</Button>
|
|
84
|
+
<Button
|
|
85
|
+
size="sm"
|
|
86
|
+
variant="ghost"
|
|
87
|
+
className="h-6 px-2 text-[10px] text-destructive hover:text-destructive"
|
|
88
|
+
disabled={busy}
|
|
89
|
+
onClick={() => void handle(onDeny)}
|
|
90
|
+
title="Deny"
|
|
91
|
+
>
|
|
92
|
+
<XCircle className="h-3 w-3 mr-0.5" />
|
|
93
|
+
Deny
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<p className="text-[10px] text-muted-foreground/50 mt-0.5">
|
|
98
|
+
Requested {relativeTime(reg.requestedAt)}
|
|
99
|
+
</p>
|
|
100
|
+
</li>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function AgentRow({
|
|
105
|
+
agent,
|
|
106
|
+
onRevoke,
|
|
107
|
+
}: {
|
|
108
|
+
agent: RegisteredAgent;
|
|
109
|
+
onRevoke: (id: string) => Promise<void>;
|
|
110
|
+
}) {
|
|
111
|
+
const [busy, setBusy] = useState(false);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<li className="py-1.5 border-b border-border/40 last:border-0">
|
|
115
|
+
<div className="flex items-start justify-between gap-1">
|
|
116
|
+
<div className="min-w-0">
|
|
117
|
+
<p className="text-xs font-mono truncate">{agent.displayName}</p>
|
|
118
|
+
<p className="text-[10px] text-muted-foreground/60 truncate">{agent.id}</p>
|
|
119
|
+
<ScopeTag scope={agent.scope} />
|
|
120
|
+
</div>
|
|
121
|
+
<Button
|
|
122
|
+
size="sm"
|
|
123
|
+
variant="ghost"
|
|
124
|
+
className="h-6 w-6 p-0 shrink-0 text-destructive hover:text-destructive"
|
|
125
|
+
disabled={busy}
|
|
126
|
+
onClick={async () => {
|
|
127
|
+
setBusy(true);
|
|
128
|
+
try {
|
|
129
|
+
await onRevoke(agent.id);
|
|
130
|
+
} finally {
|
|
131
|
+
setBusy(false);
|
|
132
|
+
}
|
|
133
|
+
}}
|
|
134
|
+
title="Revoke agent"
|
|
135
|
+
>
|
|
136
|
+
<Trash2 className="h-3 w-3" />
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
<p className="text-[10px] text-muted-foreground/50 mt-0.5">
|
|
140
|
+
Last seen {relativeTime(agent.lastSeen)}
|
|
141
|
+
</p>
|
|
142
|
+
</li>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Main component ────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export function TokenSection() {
|
|
149
|
+
const [pending, setPending] = useState<PendingReg[]>([]);
|
|
150
|
+
const [agents, setAgents] = useState<RegisteredAgent[]>([]);
|
|
151
|
+
const [ownerReady, setOwnerReady] = useState(false);
|
|
152
|
+
|
|
153
|
+
// Bootstrap owner cookie on first mount (localhost only)
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
fetch("/api/owner/init", { credentials: "same-origin" })
|
|
156
|
+
.then((r) => {
|
|
157
|
+
if (r.ok || r.status === 204) setOwnerReady(true);
|
|
158
|
+
})
|
|
159
|
+
.catch(() => {
|
|
160
|
+
// Non-localhost: owner init will fail — that's expected for remote
|
|
161
|
+
setOwnerReady(true);
|
|
162
|
+
});
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const fetchPending = useCallback(async () => {
|
|
166
|
+
try {
|
|
167
|
+
const r = await fetch("/api/agent/admin/registrations", { credentials: "same-origin" });
|
|
168
|
+
if (!r.ok) return;
|
|
169
|
+
const data = (await r.json()) as { pending: PendingReg[] };
|
|
170
|
+
setPending(data.pending);
|
|
171
|
+
} catch {
|
|
172
|
+
// Ignore fetch errors (offline, etc.)
|
|
173
|
+
}
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
const fetchAgents = useCallback(async () => {
|
|
177
|
+
try {
|
|
178
|
+
const r = await fetch("/api/agent/admin/agents", { credentials: "same-origin" });
|
|
179
|
+
if (!r.ok) return;
|
|
180
|
+
const data = (await r.json()) as { agents: RegisteredAgent[] };
|
|
181
|
+
setAgents(data.agents);
|
|
182
|
+
} catch {
|
|
183
|
+
// Ignore fetch errors
|
|
184
|
+
}
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// Poll pending every 5s, agents every 30s
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!ownerReady) return;
|
|
190
|
+
void fetchPending();
|
|
191
|
+
void fetchAgents();
|
|
192
|
+
const pendingId = setInterval(() => void fetchPending(), 5_000);
|
|
193
|
+
const agentsId = setInterval(() => void fetchAgents(), 30_000);
|
|
194
|
+
return () => {
|
|
195
|
+
clearInterval(pendingId);
|
|
196
|
+
clearInterval(agentsId);
|
|
197
|
+
};
|
|
198
|
+
}, [ownerReady, fetchPending, fetchAgents]);
|
|
199
|
+
|
|
200
|
+
const handleApprove = useCallback(
|
|
201
|
+
async (regId: string) => {
|
|
202
|
+
const r = await fetch(`/api/agent/admin/registrations/${regId}/approve`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
credentials: "same-origin",
|
|
205
|
+
headers: { "Content-Type": "application/json" },
|
|
206
|
+
body: "{}",
|
|
207
|
+
});
|
|
208
|
+
if (r.ok) {
|
|
209
|
+
toast.success("Agent approved");
|
|
210
|
+
await Promise.all([fetchPending(), fetchAgents()]);
|
|
211
|
+
} else {
|
|
212
|
+
toast.error("Failed to approve agent");
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
[fetchPending, fetchAgents],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const handleDeny = useCallback(
|
|
219
|
+
async (regId: string) => {
|
|
220
|
+
const r = await fetch(`/api/agent/admin/registrations/${regId}/deny`, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
credentials: "same-origin",
|
|
223
|
+
});
|
|
224
|
+
if (r.ok) {
|
|
225
|
+
toast.success("Registration denied");
|
|
226
|
+
await fetchPending();
|
|
227
|
+
} else {
|
|
228
|
+
toast.error("Failed to deny registration");
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
[fetchPending],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const handleRevoke = useCallback(
|
|
235
|
+
async (agentId: string) => {
|
|
236
|
+
const r = await fetch(
|
|
237
|
+
`/api/agent/admin/agents/${encodeURIComponent(agentId)}/revoke`,
|
|
238
|
+
{ method: "POST", credentials: "same-origin" },
|
|
239
|
+
);
|
|
240
|
+
if (r.ok) {
|
|
241
|
+
toast.success("Agent revoked");
|
|
242
|
+
await fetchAgents();
|
|
243
|
+
} else {
|
|
244
|
+
toast.error("Failed to revoke agent");
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
[fetchAgents],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<section className="space-y-2">
|
|
252
|
+
<div className="flex items-center justify-between">
|
|
253
|
+
<h3 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
|
254
|
+
Agents
|
|
255
|
+
</h3>
|
|
256
|
+
<Button
|
|
257
|
+
size="sm"
|
|
258
|
+
variant="ghost"
|
|
259
|
+
className="h-5 w-5 p-0"
|
|
260
|
+
title="Refresh"
|
|
261
|
+
onClick={() => void Promise.all([fetchPending(), fetchAgents()])}
|
|
262
|
+
>
|
|
263
|
+
<RefreshCw className="h-3 w-3" />
|
|
264
|
+
</Button>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Pending registrations */}
|
|
268
|
+
{pending.length > 0 && (
|
|
269
|
+
<div className="rounded-md border border-amber-500/40 bg-amber-50/10 p-2">
|
|
270
|
+
<p className="text-[10px] font-semibold uppercase tracking-widest text-amber-600/80 mb-1 flex items-center gap-1">
|
|
271
|
+
<Clock className="h-3 w-3" />
|
|
272
|
+
Pending ({pending.length})
|
|
273
|
+
</p>
|
|
274
|
+
<ul>
|
|
275
|
+
{pending.map((reg) => (
|
|
276
|
+
<PendingRow
|
|
277
|
+
key={reg.registrationId}
|
|
278
|
+
reg={reg}
|
|
279
|
+
onApprove={handleApprove}
|
|
280
|
+
onDeny={handleDeny}
|
|
281
|
+
/>
|
|
282
|
+
))}
|
|
283
|
+
</ul>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{/* Registered agents */}
|
|
288
|
+
<div className="rounded-md border border-border bg-muted/40 p-2">
|
|
289
|
+
{agents.length === 0 ? (
|
|
290
|
+
<p className="text-xs text-muted-foreground/60 py-1">
|
|
291
|
+
No registered agents.{" "}
|
|
292
|
+
<span className="text-muted-foreground/40">
|
|
293
|
+
Agents register via POST /api/agent/register.
|
|
294
|
+
</span>
|
|
295
|
+
</p>
|
|
296
|
+
) : (
|
|
297
|
+
<ul>
|
|
298
|
+
{agents.map((agent) => (
|
|
299
|
+
<AgentRow key={agent.id} agent={agent} onRevoke={handleRevoke} />
|
|
300
|
+
))}
|
|
301
|
+
</ul>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<p className="text-[10px] text-muted-foreground/60 leading-relaxed">
|
|
306
|
+
Agents authenticate via bearer token + X-Agent-Id header. Use{" "}
|
|
307
|
+
<code className="bg-muted px-0.5 rounded">POST /api/agent/register</code> to start the
|
|
308
|
+
TOFU flow.
|
|
309
|
+
</p>
|
|
310
|
+
</section>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { AlertCircle, Loader2 } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Sheet,
|
|
9
|
+
SheetContent,
|
|
10
|
+
SheetDescription,
|
|
11
|
+
SheetFooter,
|
|
12
|
+
SheetHeader,
|
|
13
|
+
SheetTitle,
|
|
14
|
+
} from "@/components/ui/sheet";
|
|
15
|
+
import { showError } from "@/lib/toast";
|
|
16
|
+
|
|
17
|
+
interface AuthSettings {
|
|
18
|
+
allowedEmails: string[];
|
|
19
|
+
allowedDomains: string[];
|
|
20
|
+
source: "config" | "env";
|
|
21
|
+
envFallbackActive: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listToText(list: string[]): string {
|
|
25
|
+
return list.join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function textToList(text: string): string[] {
|
|
29
|
+
return text
|
|
30
|
+
.split(/[\n,]/)
|
|
31
|
+
.map((s) => s.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AuthSettingsSheet({
|
|
36
|
+
open,
|
|
37
|
+
onOpenChange,
|
|
38
|
+
}: {
|
|
39
|
+
open: boolean;
|
|
40
|
+
onOpenChange: (open: boolean) => void;
|
|
41
|
+
}) {
|
|
42
|
+
const [loading, setLoading] = useState(false);
|
|
43
|
+
const [saving, setSaving] = useState(false);
|
|
44
|
+
const [error, setError] = useState<string | null>(null);
|
|
45
|
+
const [source, setSource] = useState<"config" | "env">("env");
|
|
46
|
+
const [emailsText, setEmailsText] = useState("");
|
|
47
|
+
const [domainsText, setDomainsText] = useState("");
|
|
48
|
+
|
|
49
|
+
const load = useCallback(async () => {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
setError(null);
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch("/api/system/auth-settings");
|
|
54
|
+
if (!res.ok) throw new Error("Failed to load settings");
|
|
55
|
+
const data: AuthSettings = await res.json();
|
|
56
|
+
setEmailsText(listToText(data.allowedEmails));
|
|
57
|
+
setDomainsText(listToText(data.allowedDomains));
|
|
58
|
+
setSource(data.source);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
setError(e instanceof Error ? e.message : "Failed to load");
|
|
61
|
+
} finally {
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (open) void load();
|
|
68
|
+
}, [open, load]);
|
|
69
|
+
|
|
70
|
+
async function handleSave() {
|
|
71
|
+
setSaving(true);
|
|
72
|
+
setError(null);
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch("/api/system/auth-settings", {
|
|
75
|
+
method: "PUT",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
allowedEmails: textToList(emailsText),
|
|
79
|
+
allowedDomains: textToList(domainsText),
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
const data: { error?: string; source?: "config" | "env" } =
|
|
83
|
+
await res.json();
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
setError(data.error ?? "Save failed");
|
|
86
|
+
showError(data.error ?? "Save failed");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (data.source) setSource(data.source);
|
|
90
|
+
onOpenChange(false);
|
|
91
|
+
} catch {
|
|
92
|
+
setError("Save failed");
|
|
93
|
+
} finally {
|
|
94
|
+
setSaving(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const noRestriction =
|
|
99
|
+
textToList(emailsText).length === 0 &&
|
|
100
|
+
textToList(domainsText).length === 0;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
104
|
+
<SheetContent side="right" className="w-full sm:max-w-md flex flex-col">
|
|
105
|
+
<SheetHeader>
|
|
106
|
+
<SheetTitle>Signup allowlist</SheetTitle>
|
|
107
|
+
<SheetDescription>
|
|
108
|
+
Control who can create an account. Leave both lists empty to allow
|
|
109
|
+
any email. Saving here overrides the AUTH_ALLOWED_EMAILS and
|
|
110
|
+
AUTH_ALLOWED_DOMAIN environment variables.
|
|
111
|
+
</SheetDescription>
|
|
112
|
+
</SheetHeader>
|
|
113
|
+
|
|
114
|
+
{loading ? (
|
|
115
|
+
<div className="flex flex-1 items-center justify-center">
|
|
116
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
117
|
+
</div>
|
|
118
|
+
) : (
|
|
119
|
+
<div className="flex-1 space-y-5 overflow-auto py-4">
|
|
120
|
+
{source === "env" && (
|
|
121
|
+
<div className="flex items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground">
|
|
122
|
+
<AlertCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
|
|
123
|
+
<span>
|
|
124
|
+
Current values come from environment variables. Saving moves
|
|
125
|
+
the allowlist into config and takes precedence going forward.
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
<div className="space-y-1.5">
|
|
131
|
+
<label
|
|
132
|
+
htmlFor="allowed-emails"
|
|
133
|
+
className="text-sm font-medium"
|
|
134
|
+
>
|
|
135
|
+
Allowed emails
|
|
136
|
+
</label>
|
|
137
|
+
<p className="text-xs text-muted-foreground">
|
|
138
|
+
One per line (or comma-separated). Exact match,
|
|
139
|
+
case-insensitive.
|
|
140
|
+
</p>
|
|
141
|
+
<textarea
|
|
142
|
+
id="allowed-emails"
|
|
143
|
+
value={emailsText}
|
|
144
|
+
onChange={(e) => setEmailsText(e.target.value)}
|
|
145
|
+
spellCheck={false}
|
|
146
|
+
rows={5}
|
|
147
|
+
placeholder="alice@example.com bob@example.com"
|
|
148
|
+
className="w-full resize-y rounded-md border border-border bg-background px-3 py-2 font-mono text-sm outline-none focus:ring-2 focus:ring-primary"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="space-y-1.5">
|
|
153
|
+
<label
|
|
154
|
+
htmlFor="allowed-domains"
|
|
155
|
+
className="text-sm font-medium"
|
|
156
|
+
>
|
|
157
|
+
Allowed domains
|
|
158
|
+
</label>
|
|
159
|
+
<p className="text-xs text-muted-foreground">
|
|
160
|
+
One per line (or comma-separated). Matches any email at that
|
|
161
|
+
domain.
|
|
162
|
+
</p>
|
|
163
|
+
<textarea
|
|
164
|
+
id="allowed-domains"
|
|
165
|
+
value={domainsText}
|
|
166
|
+
onChange={(e) => setDomainsText(e.target.value)}
|
|
167
|
+
spellCheck={false}
|
|
168
|
+
rows={4}
|
|
169
|
+
placeholder="example.com trusted.org"
|
|
170
|
+
className="w-full resize-y rounded-md border border-border bg-background px-3 py-2 font-mono text-sm outline-none focus:ring-2 focus:ring-primary"
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{noRestriction && (
|
|
175
|
+
<div className="flex items-start gap-2 rounded-md bg-warning-soft px-3 py-2 text-xs text-warning-ink">
|
|
176
|
+
<AlertCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
|
|
177
|
+
<span>
|
|
178
|
+
Both lists are empty. Anyone with the signup URL can create an
|
|
179
|
+
account.
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{error && (
|
|
185
|
+
<div className="flex items-center gap-1.5 text-xs text-destructive">
|
|
186
|
+
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
|
|
187
|
+
{error}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
<SheetFooter>
|
|
194
|
+
<Button
|
|
195
|
+
variant="ghost"
|
|
196
|
+
onClick={() => onOpenChange(false)}
|
|
197
|
+
disabled={saving}
|
|
198
|
+
>
|
|
199
|
+
Cancel
|
|
200
|
+
</Button>
|
|
201
|
+
<Button onClick={handleSave} disabled={saving || loading} className="gap-1.5">
|
|
202
|
+
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
|
203
|
+
Save
|
|
204
|
+
</Button>
|
|
205
|
+
</SheetFooter>
|
|
206
|
+
</SheetContent>
|
|
207
|
+
</Sheet>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
Code,
|
|
12
12
|
Italic,
|
|
13
13
|
Link as LinkIcon,
|
|
14
|
+
MessageCircle,
|
|
15
|
+
MessageSquarePlus,
|
|
14
16
|
Strikethrough,
|
|
15
17
|
Subscript as SubIcon,
|
|
16
18
|
Superscript as SuperIcon,
|
|
@@ -22,6 +24,10 @@ import { LinkPopover } from "./link-popover";
|
|
|
22
24
|
|
|
23
25
|
interface Props {
|
|
24
26
|
editor: Editor | null;
|
|
27
|
+
/** Open the human "suggest edit" flow for the current selection's block. */
|
|
28
|
+
onSuggestEdit?: () => void;
|
|
29
|
+
/** Open a comment thread on the current selection's block. */
|
|
30
|
+
onComment?: () => void;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
type OpenPopover =
|
|
@@ -34,7 +40,7 @@ type OpenPopover =
|
|
|
34
40
|
anchor: { top: number; left: number };
|
|
35
41
|
};
|
|
36
42
|
|
|
37
|
-
export function EditorBubbleMenu({ editor }: Props) {
|
|
43
|
+
export function EditorBubbleMenu({ editor, onSuggestEdit, onComment }: Props) {
|
|
38
44
|
const [popover, setPopover] = useState<OpenPopover>(null);
|
|
39
45
|
|
|
40
46
|
useEffect(() => {
|
|
@@ -58,7 +64,7 @@ export function EditorBubbleMenu({ editor }: Props) {
|
|
|
58
64
|
|
|
59
65
|
const btn = (active: boolean) =>
|
|
60
66
|
cn(
|
|
61
|
-
"h-7 w-7 flex items-center justify-center rounded hover:bg-accent text-foreground/80 transition-colors",
|
|
67
|
+
"h-9 w-9 sm:h-7 sm:w-7 flex items-center justify-center rounded hover:bg-accent text-foreground/80 transition-colors",
|
|
62
68
|
active && "bg-accent text-foreground",
|
|
63
69
|
);
|
|
64
70
|
|
|
@@ -205,6 +211,39 @@ export function EditorBubbleMenu({ editor }: Props) {
|
|
|
205
211
|
>
|
|
206
212
|
<LinkIcon className="w-3.5 h-3.5" />
|
|
207
213
|
</button>
|
|
214
|
+
{(onSuggestEdit || onComment) && (
|
|
215
|
+
<div className="w-px h-5 bg-border mx-1" />
|
|
216
|
+
)}
|
|
217
|
+
{onComment && (
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
className={btn(false)}
|
|
221
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
222
|
+
onClick={(e) => {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
onComment();
|
|
225
|
+
}}
|
|
226
|
+
aria-label="Comment"
|
|
227
|
+
title="Comment"
|
|
228
|
+
>
|
|
229
|
+
<MessageCircle className="w-3.5 h-3.5" />
|
|
230
|
+
</button>
|
|
231
|
+
)}
|
|
232
|
+
{onSuggestEdit && (
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className={btn(false)}
|
|
236
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
237
|
+
onClick={(e) => {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
onSuggestEdit();
|
|
240
|
+
}}
|
|
241
|
+
aria-label="Suggest edit"
|
|
242
|
+
title="Suggest edit"
|
|
243
|
+
>
|
|
244
|
+
<MessageSquarePlus className="w-3.5 h-3.5" />
|
|
245
|
+
</button>
|
|
246
|
+
)}
|
|
208
247
|
<div className="w-px h-5 bg-border mx-1" />
|
|
209
248
|
<div className="relative">
|
|
210
249
|
<button
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckCircle2, MessageCircle } from "lucide-react";
|
|
4
|
+
import type { Comment } from "@/lib/proof/types";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
blockRef: string;
|
|
8
|
+
comments: Comment[];
|
|
9
|
+
top: number;
|
|
10
|
+
left: number;
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gutter pip rendered absolutely inside the editor scroll container.
|
|
16
|
+
* Positioned via `top`/`left` props (pixels relative to scroll container).
|
|
17
|
+
*/
|
|
18
|
+
export function CommentPip({ blockRef, comments, top, left, onClick }: Props) {
|
|
19
|
+
if (comments.length === 0) return null;
|
|
20
|
+
|
|
21
|
+
const open = comments.filter((c) => !c.resolved);
|
|
22
|
+
const allResolved = open.length === 0;
|
|
23
|
+
|
|
24
|
+
let variant: "dot-ai" | "ring-human" | "check";
|
|
25
|
+
if (allResolved) {
|
|
26
|
+
variant = "check";
|
|
27
|
+
} else {
|
|
28
|
+
const lastTurn = open[0].turns.at(-1);
|
|
29
|
+
variant = lastTurn?.by.startsWith("ai:") ? "dot-ai" : "ring-human";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={onClick}
|
|
36
|
+
style={{
|
|
37
|
+
position: "absolute",
|
|
38
|
+
top,
|
|
39
|
+
left,
|
|
40
|
+
transform: "translateY(2px)",
|
|
41
|
+
}}
|
|
42
|
+
className="z-10 p-2 sm:p-0.5 -m-1.5 sm:m-0 rounded transition-colors hover:bg-accent focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
43
|
+
aria-label={`Comment thread for block ${blockRef}`}
|
|
44
|
+
>
|
|
45
|
+
{variant === "check" && (
|
|
46
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-muted-foreground/30" />
|
|
47
|
+
)}
|
|
48
|
+
{variant === "dot-ai" && (
|
|
49
|
+
<MessageCircle className="h-3.5 w-3.5 fill-primary text-primary" />
|
|
50
|
+
)}
|
|
51
|
+
{variant === "ring-human" && (
|
|
52
|
+
<MessageCircle className="h-3.5 w-3.5 text-muted-foreground/70" />
|
|
53
|
+
)}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|