@saadnvd1/agent-os 0.3.758 → 0.3.762
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/app/api/artifacts/[id]/route.ts +13 -4
- package/app/api/artifacts/[id]/versions/route.ts +16 -0
- package/app/api/sessions/[id]/artifacts/route.ts +9 -1
- package/components/Artifacts/ArtifactActions.tsx +308 -0
- package/components/Artifacts/ArtifactFullscreen.tsx +58 -0
- package/components/Artifacts/ArtifactPreview.tsx +141 -18
- package/components/Artifacts/ArtifactRow.tsx +5 -0
- package/components/Artifacts/ArtifactsPanel.tsx +2 -0
- package/components/Artifacts/VersionSwitcher.tsx +216 -0
- package/components/Chat/Artifact.tsx +129 -89
- package/components/Chat/ComposerMenu.tsx +14 -5
- package/components/Share/DownloadMenu.tsx +63 -38
- package/components/Share/ShareDialog.tsx +2 -0
- package/data/session-artifacts/keys.ts +3 -0
- package/data/session-artifacts/queries.ts +25 -0
- package/lib/artifacts/actions.ts +75 -0
- package/lib/artifacts/store.ts +105 -10
- package/lib/artifacts/tools.ts +29 -6
- package/lib/artifacts/versions.ts +73 -0
- package/lib/chat/commands.ts +9 -0
- package/lib/chat/drivers/claude-mapper.ts +2 -2
- package/lib/chat/events.ts +4 -0
- package/lib/chat/worker/host.ts +2 -0
- package/lib/db/migrations/20261029T1200_artifact_versions.ts +28 -0
- package/lib/security/demo.ts +1 -0
- package/lib/session-artifacts/collect.ts +28 -6
- package/lib/session-artifacts/derive.ts +3 -1
- package/lib/session-artifacts/types.ts +9 -0
- package/next.config.ts +6 -0
- package/package.json +3 -3
- package/stores/shareUi.ts +6 -2
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getArtifact,
|
|
4
|
+
latestVersion,
|
|
5
|
+
readArtifactHtml,
|
|
6
|
+
} from "@/lib/artifacts/store";
|
|
3
7
|
import { ARTIFACT_HEADERS, prepareArtifact } from "@/lib/artifacts/serve";
|
|
4
8
|
|
|
5
|
-
// GET /api/artifacts/[id] - a page an agent showed, sandboxed
|
|
9
|
+
// GET /api/artifacts/[id] - a page an agent showed, sandboxed. That exact
|
|
10
|
+
// version, or with ?v=latest the page's newest.
|
|
6
11
|
export async function GET(
|
|
7
|
-
|
|
12
|
+
request: Request,
|
|
8
13
|
{ params }: { params: Promise<{ id: string }> }
|
|
9
14
|
) {
|
|
10
15
|
const { id } = await params;
|
|
11
|
-
const
|
|
16
|
+
const asked = getArtifact(id);
|
|
17
|
+
const artifact =
|
|
18
|
+
asked && new URL(request.url).searchParams.get("v") === "latest"
|
|
19
|
+
? latestVersion(asked.page_id)
|
|
20
|
+
: asked;
|
|
12
21
|
const html = artifact ? await readArtifactHtml(artifact) : null;
|
|
13
22
|
if (html === null)
|
|
14
23
|
return NextResponse.json({ error: "Artifact not found" }, { status: 404 });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { pageVersions } from "@/lib/artifacts/versions";
|
|
3
|
+
import { pdfAvailable } from "@/lib/share/pdf";
|
|
4
|
+
|
|
5
|
+
// GET /api/artifacts/[id]/versions - every version of the page this is one
|
|
6
|
+
// of, oldest first, and what can be done with it
|
|
7
|
+
export async function GET(
|
|
8
|
+
_request: Request,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const page = pageVersions(id, { pdf: await pdfAvailable() });
|
|
13
|
+
if (!page)
|
|
14
|
+
return NextResponse.json({ error: "Artifact not found" }, { status: 404 });
|
|
15
|
+
return NextResponse.json(page);
|
|
16
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { artifactActions } from "@/lib/artifacts/actions";
|
|
3
|
+
import { pdfAvailable } from "@/lib/share/pdf";
|
|
2
4
|
import { peerLinkOf } from "@/lib/hosts/peer-actions";
|
|
3
5
|
import { collectArtifacts, getSession } from "@/lib/session-artifacts/collect";
|
|
4
6
|
import { peerArtifacts } from "@/lib/session-artifacts/peer";
|
|
@@ -29,5 +31,11 @@ export async function GET(
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
const entries = await collectArtifacts(session);
|
|
32
|
-
|
|
34
|
+
const ctx = { pdf: await pdfAvailable() };
|
|
35
|
+
return NextResponse.json({
|
|
36
|
+
artifacts: entries.map((e) => ({
|
|
37
|
+
...e.artifact,
|
|
38
|
+
actions: artifactActions(e.artifact, ctx),
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
33
41
|
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Download,
|
|
7
|
+
ExternalLink,
|
|
8
|
+
Loader2,
|
|
9
|
+
Maximize2,
|
|
10
|
+
MoreHorizontal,
|
|
11
|
+
Share2,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import { Button } from "@/components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
} from "@/components/ui/dropdown-menu";
|
|
20
|
+
import { artifactUrl } from "@/components/Chat/ArtifactFrame";
|
|
21
|
+
import {
|
|
22
|
+
DownloadMenu,
|
|
23
|
+
useShareDownload,
|
|
24
|
+
} from "@/components/Share/DownloadMenu";
|
|
25
|
+
import type { PreviewRequest } from "@/data/shares";
|
|
26
|
+
import type { ArtifactAction, DownloadFormat } from "@/lib/artifacts/actions";
|
|
27
|
+
import { shareUiActions } from "@/stores/shareUi";
|
|
28
|
+
import { cn } from "@/lib/utils";
|
|
29
|
+
|
|
30
|
+
export const ICON_BUTTON =
|
|
31
|
+
"text-muted-foreground hover:text-foreground hover:bg-foreground/[0.06] inline-flex h-11 w-11 items-center justify-center rounded-lg md:h-8 md:w-8";
|
|
32
|
+
|
|
33
|
+
const FORMAT_LABEL: Record<DownloadFormat, string> = {
|
|
34
|
+
pdf: "Download PDF",
|
|
35
|
+
html: "Download HTML page",
|
|
36
|
+
md: "Download Markdown",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
// The exact version every action acts on.
|
|
41
|
+
artifactId: string;
|
|
42
|
+
title: string;
|
|
43
|
+
// Named in the share dialog when the page has more than one.
|
|
44
|
+
version?: number;
|
|
45
|
+
actions: ArtifactAction[];
|
|
46
|
+
// icons: the chat card's title bar. labelled: the Artifacts view's header.
|
|
47
|
+
// bar: the Artifacts view's bottom bar on a phone.
|
|
48
|
+
variant: "icons" | "labelled" | "bar";
|
|
49
|
+
onFull?: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A rendered page's actions: full screen, share, open, download. One place
|
|
53
|
+
// for the chat card and the Artifacts view, from the server's list.
|
|
54
|
+
export function ArtifactActions({
|
|
55
|
+
artifactId,
|
|
56
|
+
title,
|
|
57
|
+
version,
|
|
58
|
+
actions,
|
|
59
|
+
variant,
|
|
60
|
+
onFull,
|
|
61
|
+
}: Props) {
|
|
62
|
+
const has = (id: ArtifactAction["id"]) => actions.some((a) => a.id === id);
|
|
63
|
+
const formats = actions.find((a) => a.id === "download")?.formats ?? [];
|
|
64
|
+
const full = has("fullscreen") && onFull;
|
|
65
|
+
const request: PreviewRequest = {
|
|
66
|
+
kind: "artifact",
|
|
67
|
+
artifactId,
|
|
68
|
+
includeTools: false,
|
|
69
|
+
};
|
|
70
|
+
const href = artifactUrl(artifactId);
|
|
71
|
+
const share = () =>
|
|
72
|
+
shareUiActions.open({ kind: "artifact", artifactId, title, version });
|
|
73
|
+
|
|
74
|
+
if (variant === "bar")
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex gap-2">
|
|
77
|
+
{has("share") && (
|
|
78
|
+
<Button className="h-11 flex-1" onClick={share}>
|
|
79
|
+
<Share2 className="h-4 w-4" /> Share
|
|
80
|
+
</Button>
|
|
81
|
+
)}
|
|
82
|
+
{has("open") && (
|
|
83
|
+
<Button variant="outline" className="h-11 flex-1" asChild>
|
|
84
|
+
<a href={href} target="_blank" rel="noopener noreferrer">
|
|
85
|
+
<ExternalLink className="h-4 w-4" /> Open
|
|
86
|
+
</a>
|
|
87
|
+
</Button>
|
|
88
|
+
)}
|
|
89
|
+
{full && (
|
|
90
|
+
<Button
|
|
91
|
+
variant="outline"
|
|
92
|
+
className="h-11 w-11 shrink-0"
|
|
93
|
+
onClick={onFull}
|
|
94
|
+
aria-label="Full screen"
|
|
95
|
+
>
|
|
96
|
+
<Maximize2 className="h-4 w-4" />
|
|
97
|
+
</Button>
|
|
98
|
+
)}
|
|
99
|
+
{formats.length > 0 && (
|
|
100
|
+
<More request={request} title={title} formats={formats}>
|
|
101
|
+
{(pending) => (
|
|
102
|
+
<Button
|
|
103
|
+
variant="outline"
|
|
104
|
+
className="h-11 w-11 shrink-0"
|
|
105
|
+
aria-label="More"
|
|
106
|
+
>
|
|
107
|
+
{pending ? (
|
|
108
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
109
|
+
) : (
|
|
110
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
111
|
+
)}
|
|
112
|
+
</Button>
|
|
113
|
+
)}
|
|
114
|
+
</More>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (variant === "labelled") {
|
|
120
|
+
const label = "hidden @lg:inline";
|
|
121
|
+
return (
|
|
122
|
+
<div className="flex shrink-0 gap-1.5">
|
|
123
|
+
{full && (
|
|
124
|
+
<Button
|
|
125
|
+
variant="outline"
|
|
126
|
+
size="sm"
|
|
127
|
+
onClick={onFull}
|
|
128
|
+
title="Full screen"
|
|
129
|
+
>
|
|
130
|
+
<Maximize2 className="h-3.5 w-3.5" />
|
|
131
|
+
<span className={label}>Full screen</span>
|
|
132
|
+
</Button>
|
|
133
|
+
)}
|
|
134
|
+
{has("share") && (
|
|
135
|
+
<Button variant="outline" size="sm" onClick={share} title="Share">
|
|
136
|
+
<Share2 className="h-3.5 w-3.5" />
|
|
137
|
+
<span className={label}>Share</span>
|
|
138
|
+
</Button>
|
|
139
|
+
)}
|
|
140
|
+
{has("open") && (
|
|
141
|
+
<Button variant="outline" size="sm" asChild>
|
|
142
|
+
<a
|
|
143
|
+
href={href}
|
|
144
|
+
target="_blank"
|
|
145
|
+
rel="noopener noreferrer"
|
|
146
|
+
title="Open in new tab"
|
|
147
|
+
>
|
|
148
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
149
|
+
<span className={label}>Open</span>
|
|
150
|
+
</a>
|
|
151
|
+
</Button>
|
|
152
|
+
)}
|
|
153
|
+
{formats.length > 0 && (
|
|
154
|
+
<DownloadMenu
|
|
155
|
+
formats={formats}
|
|
156
|
+
request={request}
|
|
157
|
+
title={title}
|
|
158
|
+
disabled={false}
|
|
159
|
+
trigger={(pending) => (
|
|
160
|
+
<Button variant="outline" size="sm" title="Download">
|
|
161
|
+
{pending ? (
|
|
162
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
163
|
+
) : (
|
|
164
|
+
<Download className="h-3.5 w-3.5" />
|
|
165
|
+
)}
|
|
166
|
+
<span className={label}>Download</span>
|
|
167
|
+
</Button>
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The card: every action as an icon from md up; on a phone, Share and a
|
|
176
|
+
// More menu, so the title keeps its room.
|
|
177
|
+
const wide = "hidden md:inline-flex";
|
|
178
|
+
return (
|
|
179
|
+
<div className="flex shrink-0 items-center">
|
|
180
|
+
{full && (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={onFull}
|
|
184
|
+
className={cn(ICON_BUTTON, wide)}
|
|
185
|
+
aria-label="Full screen"
|
|
186
|
+
title="Full screen"
|
|
187
|
+
>
|
|
188
|
+
<Maximize2 className="h-4 w-4" />
|
|
189
|
+
</button>
|
|
190
|
+
)}
|
|
191
|
+
{has("share") && (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={share}
|
|
195
|
+
className={ICON_BUTTON}
|
|
196
|
+
aria-label="Share"
|
|
197
|
+
title="Share"
|
|
198
|
+
>
|
|
199
|
+
<Share2 className="h-4 w-4" />
|
|
200
|
+
</button>
|
|
201
|
+
)}
|
|
202
|
+
{has("open") && (
|
|
203
|
+
<a
|
|
204
|
+
href={href}
|
|
205
|
+
target="_blank"
|
|
206
|
+
rel="noopener noreferrer"
|
|
207
|
+
className={cn(ICON_BUTTON, wide)}
|
|
208
|
+
aria-label="Open in new tab"
|
|
209
|
+
title="Open in new tab"
|
|
210
|
+
>
|
|
211
|
+
<ExternalLink className="h-4 w-4" />
|
|
212
|
+
</a>
|
|
213
|
+
)}
|
|
214
|
+
{formats.length > 0 && (
|
|
215
|
+
<DownloadMenu
|
|
216
|
+
formats={formats}
|
|
217
|
+
request={request}
|
|
218
|
+
title={title}
|
|
219
|
+
disabled={false}
|
|
220
|
+
trigger={(pending) => (
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
className={cn(ICON_BUTTON, wide)}
|
|
224
|
+
aria-label="Download"
|
|
225
|
+
title="Download"
|
|
226
|
+
>
|
|
227
|
+
{pending ? (
|
|
228
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
229
|
+
) : (
|
|
230
|
+
<Download className="h-4 w-4" />
|
|
231
|
+
)}
|
|
232
|
+
</button>
|
|
233
|
+
)}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
<More
|
|
237
|
+
request={request}
|
|
238
|
+
title={title}
|
|
239
|
+
formats={formats}
|
|
240
|
+
onFull={full ? onFull : undefined}
|
|
241
|
+
href={has("open") ? href : undefined}
|
|
242
|
+
>
|
|
243
|
+
{(pending) => (
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
className={cn(ICON_BUTTON, "md:hidden")}
|
|
247
|
+
aria-label="More"
|
|
248
|
+
>
|
|
249
|
+
{pending ? (
|
|
250
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
251
|
+
) : (
|
|
252
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
253
|
+
)}
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
</More>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The phone's overflow menu: full screen and open when they didn't fit, then
|
|
262
|
+
// each download format.
|
|
263
|
+
function More({
|
|
264
|
+
request,
|
|
265
|
+
title,
|
|
266
|
+
formats,
|
|
267
|
+
onFull,
|
|
268
|
+
href,
|
|
269
|
+
children,
|
|
270
|
+
}: {
|
|
271
|
+
request: PreviewRequest;
|
|
272
|
+
title: string;
|
|
273
|
+
formats: DownloadFormat[];
|
|
274
|
+
onFull?: () => void;
|
|
275
|
+
href?: string;
|
|
276
|
+
children: (pending: boolean) => ReactNode;
|
|
277
|
+
}) {
|
|
278
|
+
const { choose, pending } = useShareDownload(request, title);
|
|
279
|
+
if (!onFull && !href && !formats.length) return null;
|
|
280
|
+
return (
|
|
281
|
+
<DropdownMenu>
|
|
282
|
+
<DropdownMenuTrigger asChild>{children(pending)}</DropdownMenuTrigger>
|
|
283
|
+
<DropdownMenuContent align="end" className="min-w-52">
|
|
284
|
+
{onFull && (
|
|
285
|
+
<DropdownMenuItem className="min-h-11 gap-2" onSelect={onFull}>
|
|
286
|
+
<Maximize2 className="h-4 w-4" /> Full screen
|
|
287
|
+
</DropdownMenuItem>
|
|
288
|
+
)}
|
|
289
|
+
{href && (
|
|
290
|
+
<DropdownMenuItem className="min-h-11 gap-2" asChild>
|
|
291
|
+
<a href={href} target="_blank" rel="noopener noreferrer">
|
|
292
|
+
<ExternalLink className="h-4 w-4" /> Open in new tab
|
|
293
|
+
</a>
|
|
294
|
+
</DropdownMenuItem>
|
|
295
|
+
)}
|
|
296
|
+
{formats.map((f) => (
|
|
297
|
+
<DropdownMenuItem
|
|
298
|
+
key={f}
|
|
299
|
+
className="min-h-11 gap-2"
|
|
300
|
+
onSelect={() => void choose(f)}
|
|
301
|
+
>
|
|
302
|
+
<Download className="h-4 w-4" /> {FORMAT_LABEL[f]}
|
|
303
|
+
</DropdownMenuItem>
|
|
304
|
+
))}
|
|
305
|
+
</DropdownMenuContent>
|
|
306
|
+
</DropdownMenu>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|
6
|
+
import { ArtifactFrame } from "@/components/Chat/ArtifactFrame";
|
|
7
|
+
import { ICON_BUTTON } from "./ArtifactActions";
|
|
8
|
+
|
|
9
|
+
// A rendered page over the whole screen, its actions in the bar.
|
|
10
|
+
export function ArtifactFullscreen({
|
|
11
|
+
open,
|
|
12
|
+
onOpenChange,
|
|
13
|
+
artifactId,
|
|
14
|
+
src,
|
|
15
|
+
title,
|
|
16
|
+
actions,
|
|
17
|
+
}: {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
artifactId: string;
|
|
21
|
+
src?: string;
|
|
22
|
+
title: string;
|
|
23
|
+
actions?: ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
27
|
+
<DialogContent
|
|
28
|
+
showCloseButton={false}
|
|
29
|
+
className="flex h-[100dvh] w-screen max-w-none flex-col gap-0 rounded-none p-0 sm:max-w-none"
|
|
30
|
+
>
|
|
31
|
+
<div className="flex items-center gap-2 py-1 pt-[max(0.25rem,env(safe-area-inset-top))] pr-1 pl-4">
|
|
32
|
+
<DialogTitle className="min-w-0 flex-1 truncate text-sm font-medium">
|
|
33
|
+
{title}
|
|
34
|
+
</DialogTitle>
|
|
35
|
+
{actions}
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => onOpenChange(false)}
|
|
39
|
+
className={ICON_BUTTON}
|
|
40
|
+
aria-label="Close"
|
|
41
|
+
>
|
|
42
|
+
<X className="h-4 w-4" />
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="min-h-0 flex-1 pb-[env(safe-area-inset-bottom)]">
|
|
46
|
+
{open && (
|
|
47
|
+
<ArtifactFrame
|
|
48
|
+
artifactId={artifactId}
|
|
49
|
+
src={src}
|
|
50
|
+
title={title}
|
|
51
|
+
fill
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</DialogContent>
|
|
56
|
+
</Dialog>
|
|
57
|
+
);
|
|
58
|
+
}
|