@saadnvd1/agent-os 0.3.770 → 0.3.775

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 (52) hide show
  1. package/app/api/sessions/[id]/artifacts/[artifactId]/compare/route.ts +41 -0
  2. package/app/api/shares/preview/route.ts +6 -1
  3. package/app/api/shares/route.test.ts +51 -0
  4. package/app/page.tsx +1 -0
  5. package/components/Artifacts/ArtifactActions.tsx +27 -3
  6. package/components/Artifacts/ArtifactPreview.tsx +27 -12
  7. package/components/Artifacts/Compare/DiffView.tsx +116 -0
  8. package/components/Artifacts/Compare/SideBySide.tsx +60 -0
  9. package/components/Artifacts/Compare/VersionPicker.tsx +47 -0
  10. package/components/Artifacts/Compare/index.tsx +191 -0
  11. package/components/Chat/Messages.tsx +6 -0
  12. package/components/Chat/ShareFromHere.tsx +39 -0
  13. package/components/CommandPalette/CommandPalette.tsx +5 -4
  14. package/components/CommandPalette/CommandsBody.tsx +97 -131
  15. package/components/CommandPalette/PaletteRows.tsx +79 -0
  16. package/components/CommandPalette/PaletteTabs.tsx +51 -0
  17. package/components/CommandPalette/conversations/ConversationSearch.tsx +13 -11
  18. package/components/CommandPalette/useAppCommands.ts +14 -0
  19. package/components/CommandPalette/useConversationRows.ts +55 -0
  20. package/components/SessionList/useSessionSwitching.ts +2 -2
  21. package/components/Share/MessageList.tsx +87 -0
  22. package/components/Share/RangePicker.tsx +59 -0
  23. package/components/Share/ShareDialog.tsx +113 -52
  24. package/data/session-artifacts/keys.ts +2 -0
  25. package/data/session-artifacts/queries.ts +25 -0
  26. package/data/shares/queries.ts +11 -0
  27. package/hooks/usePaletteCommands.ts +4 -1
  28. package/lib/artifacts/actions.ts +11 -1
  29. package/lib/artifacts/compare/compareVersions.ts +66 -0
  30. package/lib/artifacts/compare/diff.ts +178 -0
  31. package/lib/artifacts/compare/extractText.ts +64 -0
  32. package/lib/artifacts/compare/index.ts +11 -0
  33. package/lib/artifacts/compare/types.ts +44 -0
  34. package/lib/artifacts/match.ts +64 -0
  35. package/lib/artifacts/store.ts +39 -10
  36. package/lib/artifacts/tools.ts +9 -4
  37. package/lib/artifacts/versions.ts +2 -1
  38. package/lib/palette/registry.ts +71 -17
  39. package/lib/palette/sections.ts +143 -0
  40. package/lib/palette/tabs.ts +21 -0
  41. package/lib/palette/usage.ts +81 -0
  42. package/lib/security/demo.ts +3 -0
  43. package/lib/share/markdown.ts +3 -0
  44. package/lib/share/range.ts +189 -0
  45. package/lib/share/render.tsx +3 -0
  46. package/lib/share/service.ts +64 -19
  47. package/lib/share/snapshot.ts +39 -2
  48. package/lib/share/styles.ts +2 -0
  49. package/lib/share/view.ts +11 -0
  50. package/package.json +1 -1
  51. package/stores/palette.ts +13 -11
  52. package/stores/shareUi.ts +6 -1
@@ -0,0 +1,41 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { compareVersions } from "@/lib/artifacts/compare";
4
+ import { getSession } from "@/lib/session-artifacts/collect";
5
+
6
+ const Query = z.object({
7
+ to: z.string().min(1).max(64),
8
+ mode: z.enum(["changes", "source"]).default("changes"),
9
+ });
10
+
11
+ // GET /api/sessions/[id]/artifacts/[artifactId]/compare?to=<id>&mode= - what
12
+ // changed between two versions of one page: the visible text (changes) or the
13
+ // HTML (source). Both versions must be this session's, of the same page.
14
+ export async function GET(
15
+ request: Request,
16
+ { params }: { params: Promise<{ id: string; artifactId: string }> }
17
+ ) {
18
+ const { id, artifactId } = await params;
19
+ const query = Query.safeParse(
20
+ Object.fromEntries(new URL(request.url).searchParams)
21
+ );
22
+ if (!query.success)
23
+ return NextResponse.json(
24
+ { error: "Name the version: ?to=" },
25
+ { status: 400 }
26
+ );
27
+ if (!getSession(id))
28
+ return NextResponse.json({ error: "Session not found" }, { status: 404 });
29
+ const result = await compareVersions(
30
+ id,
31
+ artifactId,
32
+ query.data.to,
33
+ query.data.mode
34
+ );
35
+ if (!result.ok)
36
+ return NextResponse.json(
37
+ { error: result.error },
38
+ { status: result.status }
39
+ );
40
+ return NextResponse.json(result);
41
+ }
@@ -3,7 +3,7 @@ import { pdfAvailable } from "@/lib/share/pdf";
3
3
  import { linked } from "@/lib/share/connect";
4
4
  import { readJson, refuseDemo, toResponse } from "@/lib/share/route";
5
5
  import type { ShareFormat } from "@/lib/share/view";
6
- import { buildPage, parseRequest } from "@/lib/share/service";
6
+ import { buildPage, listChatItems, parseRequest } from "@/lib/share/service";
7
7
 
8
8
  // POST /api/shares/preview - exactly what a share would upload, redacted.
9
9
  // Local work only: nothing goes to Connect until a share is created.
@@ -14,6 +14,11 @@ export async function POST(request: Request) {
14
14
  if ("refused" in read) return read.refused;
15
15
  try {
16
16
  const req = parseRequest(read.body);
17
+ if (
18
+ req.kind === "chat" &&
19
+ (read.body as { listOnly?: unknown }).listOnly === true
20
+ )
21
+ return NextResponse.json({ items: await listChatItems(req) });
17
22
  const page = await buildPage(req);
18
23
  const formats: ShareFormat[] = ["html", "md"];
19
24
  if (await pdfAvailable()) formats.unshift("pdf");
@@ -316,6 +316,57 @@ describe("create", () => {
316
316
  expect(JSON.parse(seen[0].body).expires_in).toBe("never");
317
317
  });
318
318
 
319
+ it("uploads exactly the previewed range, and refuses another range's hash", async () => {
320
+ const id = chat();
321
+ const range = { fromId: "a1", toId: "a1" };
322
+ const p = await previewOf(id, range);
323
+ expect(p.html).not.toContain("[secret]");
324
+ expect(p.html).toContain("Earlier messages not shared");
325
+ expect(p.markdown).toContain("Earlier messages not shared");
326
+ const body = {
327
+ kind: "chat",
328
+ sessionId: id,
329
+ includeTools: false,
330
+ sharedAt: p.sharedAt,
331
+ hash: p.hash,
332
+ expiresIn: "7d",
333
+ };
334
+ const other = await shares.POST(
335
+ post("/api/shares", { ...body, fromId: "u1", toId: "a1" })
336
+ );
337
+ expect(other.status).toBe(409);
338
+ expect(seen).toHaveLength(0);
339
+ const res = await shares.POST(post("/api/shares", { ...body, ...range }));
340
+ expect(res.status).toBe(201);
341
+ expect(JSON.parse(seen[0].body).html).toContain(
342
+ "Earlier messages not shared"
343
+ );
344
+ });
345
+
346
+ it("lists every message with its time, and refuses a range that's gone", async () => {
347
+ const id = chat();
348
+ const res = await preview.POST(
349
+ post("/api/shares/preview", {
350
+ kind: "chat",
351
+ sessionId: id,
352
+ includeTools: false,
353
+ listOnly: true,
354
+ })
355
+ );
356
+ const { items } = await res.json();
357
+ expect(items.map((i: { id: string }) => i.id)).toEqual(["u1", "a1"]);
358
+ expect(items[0].createdAt).toBeGreaterThan(0);
359
+ const gone = await preview.POST(
360
+ post("/api/shares/preview", {
361
+ kind: "chat",
362
+ sessionId: id,
363
+ includeTools: false,
364
+ fromId: "nope",
365
+ })
366
+ );
367
+ expect(gone.status).toBe(409);
368
+ });
369
+
319
370
  it("shares an artifact", async () => {
320
371
  const id = chat();
321
372
  const art = createArtifact(
package/app/page.tsx CHANGED
@@ -721,6 +721,7 @@ function HomeContent() {
721
721
 
722
722
  useAppCommands({
723
723
  sessions,
724
+ statuses: sessionStatuses,
724
725
  onSelectSession: selectSession,
725
726
  onSearchCode: () => setShowQuickSwitcher(true),
726
727
  });
@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
5
5
  import {
6
6
  Download,
7
7
  ExternalLink,
8
+ GitCompare,
8
9
  Loader2,
9
10
  Maximize2,
10
11
  MoreHorizontal,
@@ -47,6 +48,8 @@ interface Props {
47
48
  // bar: the Artifacts view's bottom bar on a phone.
48
49
  variant: "icons" | "labelled" | "bar";
49
50
  onFull?: () => void;
51
+ // Offered only by the owner's viewer, for a page with 2+ versions.
52
+ onCompare?: () => void;
50
53
  }
51
54
 
52
55
  // A rendered page's actions: full screen, share, open, download. One place
@@ -58,10 +61,12 @@ export function ArtifactActions({
58
61
  actions,
59
62
  variant,
60
63
  onFull,
64
+ onCompare,
61
65
  }: Props) {
62
66
  const has = (id: ArtifactAction["id"]) => actions.some((a) => a.id === id);
63
67
  const formats = actions.find((a) => a.id === "download")?.formats ?? [];
64
68
  const full = has("fullscreen") && onFull;
69
+ const compare = has("compare") ? onCompare : undefined;
65
70
  const request: PreviewRequest = {
66
71
  kind: "artifact",
67
72
  artifactId,
@@ -96,8 +101,13 @@ export function ArtifactActions({
96
101
  <Maximize2 className="h-4 w-4" />
97
102
  </Button>
98
103
  )}
99
- {formats.length > 0 && (
100
- <More request={request} title={title} formats={formats}>
104
+ {(formats.length > 0 || compare) && (
105
+ <More
106
+ request={request}
107
+ title={title}
108
+ formats={formats}
109
+ onCompare={compare}
110
+ >
101
111
  {(pending) => (
102
112
  <Button
103
113
  variant="outline"
@@ -131,6 +141,12 @@ export function ArtifactActions({
131
141
  <span className={label}>Full screen</span>
132
142
  </Button>
133
143
  )}
144
+ {compare && (
145
+ <Button variant="outline" size="sm" onClick={compare} title="Compare">
146
+ <GitCompare className="h-3.5 w-3.5" />
147
+ <span className={label}>Compare</span>
148
+ </Button>
149
+ )}
134
150
  {has("share") && (
135
151
  <Button variant="outline" size="sm" onClick={share} title="Share">
136
152
  <Share2 className="h-3.5 w-3.5" />
@@ -238,6 +254,7 @@ export function ArtifactActions({
238
254
  title={title}
239
255
  formats={formats}
240
256
  onFull={full ? onFull : undefined}
257
+ onCompare={compare}
241
258
  href={has("open") ? href : undefined}
242
259
  >
243
260
  {(pending) => (
@@ -265,6 +282,7 @@ function More({
265
282
  title,
266
283
  formats,
267
284
  onFull,
285
+ onCompare,
268
286
  href,
269
287
  children,
270
288
  }: {
@@ -272,11 +290,12 @@ function More({
272
290
  title: string;
273
291
  formats: DownloadFormat[];
274
292
  onFull?: () => void;
293
+ onCompare?: () => void;
275
294
  href?: string;
276
295
  children: (pending: boolean) => ReactNode;
277
296
  }) {
278
297
  const { choose, pending } = useShareDownload(request, title);
279
- if (!onFull && !href && !formats.length) return null;
298
+ if (!onFull && !onCompare && !href && !formats.length) return null;
280
299
  return (
281
300
  <DropdownMenu>
282
301
  <DropdownMenuTrigger asChild>{children(pending)}</DropdownMenuTrigger>
@@ -286,6 +305,11 @@ function More({
286
305
  <Maximize2 className="h-4 w-4" /> Full screen
287
306
  </DropdownMenuItem>
288
307
  )}
308
+ {onCompare && (
309
+ <DropdownMenuItem className="min-h-11 gap-2" onSelect={onCompare}>
310
+ <GitCompare className="h-4 w-4" /> Compare versions
311
+ </DropdownMenuItem>
312
+ )}
289
313
  {href && (
290
314
  <DropdownMenuItem className="min-h-11 gap-2" asChild>
291
315
  <a href={href} target="_blank" rel="noopener noreferrer">
@@ -26,6 +26,7 @@ import { ArtifactIcon } from "./ArtifactRow";
26
26
  import { stateLabel } from "./helpers";
27
27
  import { ArtifactActions } from "./ArtifactActions";
28
28
  import { ArtifactFullscreen } from "./ArtifactFullscreen";
29
+ import { ArtifactCompare } from "./Compare";
29
30
  import { VersionSwitcher } from "./VersionSwitcher";
30
31
 
31
32
  function Body({
@@ -131,6 +132,7 @@ export function ArtifactPreview({
131
132
  }) {
132
133
  const [copied, setCopied] = useState(false);
133
134
  const [full, setFull] = useState(false);
135
+ const [comparing, setComparing] = useState(false);
134
136
  // The version picked; none means the latest, which follows new versions.
135
137
  const [picked, setPicked] = useState<string | null>(null);
136
138
  const page =
@@ -151,6 +153,7 @@ export function ArtifactPreview({
151
153
  const many = versions.length > 1;
152
154
  const older = many && !!shown && shown.id !== latest?.id;
153
155
 
156
+ const showCompare = comparing && many;
154
157
  const label = stateLabel(artifact, pr);
155
158
  // Inside the worktree, the path from its top; the full one is copied.
156
159
  const where =
@@ -247,6 +250,7 @@ export function ArtifactPreview({
247
250
  actions={pageList}
248
251
  variant={variant}
249
252
  onFull={() => setFull(true)}
253
+ onCompare={() => setComparing(true)}
250
254
  />
251
255
  );
252
256
  const actions = isPage
@@ -297,15 +301,15 @@ export function ArtifactPreview({
297
301
  <div className="flex shrink-0 gap-1.5">{actions}</div>
298
302
  )}
299
303
  </div>
300
- {mobile && switcher && (
304
+ {mobile && switcher && !showCompare && (
301
305
  <div className="bg-card border-b px-1">{switcher}</div>
302
306
  )}
303
- {!mobile && switcher && (
307
+ {!mobile && switcher && !showCompare && (
304
308
  <div className="flex border-b px-3 py-1.5 sm:px-4 @3xl:hidden">
305
309
  {switcher}
306
310
  </div>
307
311
  )}
308
- {older && latest && (
312
+ {older && latest && !showCompare && (
309
313
  <div className="flex min-h-11 items-center gap-2 border-b border-amber-500/25 bg-amber-500/[0.08] px-3 text-[12.5px] sm:px-4">
310
314
  <History className="h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
311
315
  <span className="min-w-0 flex-1 truncate">
@@ -324,15 +328,26 @@ export function ArtifactPreview({
324
328
  </Button>
325
329
  </div>
326
330
  )}
327
- <div className="min-h-0 flex-1 overflow-auto">
328
- <Body
329
- sessionId={sessionId}
330
- artifact={artifact}
331
- pr={pr}
332
- versionId={shownId ?? undefined}
333
- />
334
- </div>
335
- {mobile && hasActions && (
331
+ {showCompare && shownId ? (
332
+ <div className="min-h-0 flex-1">
333
+ <ArtifactCompare
334
+ sessionId={sessionId}
335
+ versions={versions}
336
+ shownId={shownId}
337
+ onClose={() => setComparing(false)}
338
+ />
339
+ </div>
340
+ ) : (
341
+ <div className="min-h-0 flex-1 overflow-auto">
342
+ <Body
343
+ sessionId={sessionId}
344
+ artifact={artifact}
345
+ pr={pr}
346
+ versionId={shownId ?? undefined}
347
+ />
348
+ </div>
349
+ )}
350
+ {mobile && hasActions && !showCompare && (
336
351
  <div className="flex gap-2 border-t px-3 pt-2.5 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
337
352
  {isPage ? <div className="flex-1">{actions}</div> : actions}
338
353
  </div>
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { cn } from "@/lib/utils";
5
+ import { foldUnchanged } from "@/lib/artifacts/compare/diff";
6
+ import type { DiffLine, WordPart } from "@/lib/artifacts/compare/types";
7
+
8
+ const ROW = {
9
+ eq: "",
10
+ add: "bg-emerald-500/10",
11
+ del: "bg-red-500/10",
12
+ } as const;
13
+ const SIGN = {
14
+ eq: " ",
15
+ add: "+",
16
+ del: "−",
17
+ } as const;
18
+ const SIGN_TONE = {
19
+ eq: "text-muted-foreground",
20
+ add: "text-emerald-700 dark:text-emerald-400",
21
+ del: "text-red-700 dark:text-red-400",
22
+ } as const;
23
+ const WORD = {
24
+ add: "bg-emerald-500/30",
25
+ del: "bg-red-500/30 line-through decoration-red-500/60",
26
+ } as const;
27
+
28
+ function Words({ parts }: { parts: WordPart[] }) {
29
+ return (
30
+ <>
31
+ {parts.map((p, i) =>
32
+ p.t === "eq" ? (
33
+ <span key={i}>{p.s}</span>
34
+ ) : (
35
+ <mark key={i} className={cn("rounded-sm text-inherit", WORD[p.t])}>
36
+ {p.s}
37
+ </mark>
38
+ )
39
+ )}
40
+ </>
41
+ );
42
+ }
43
+
44
+ function Row({ line, wrap }: { line: DiffLine; wrap: boolean }) {
45
+ return (
46
+ <div
47
+ className={cn(
48
+ "grid grid-cols-[1.75rem_1fr] font-mono text-xs leading-relaxed",
49
+ ROW[line.t]
50
+ )}
51
+ >
52
+ <span
53
+ aria-hidden
54
+ className={cn("text-center select-none", SIGN_TONE[line.t])}
55
+ >
56
+ {SIGN[line.t]}
57
+ </span>
58
+ <span
59
+ className={cn(
60
+ "min-w-0 pr-3",
61
+ wrap ? "break-words whitespace-pre-wrap" : "whitespace-pre"
62
+ )}
63
+ >
64
+ {line.words ? <Words parts={line.words} /> : line.s}
65
+ </span>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ // A unified diff: removed lines, then added, with unchanged stretches folded.
71
+ export function DiffView({
72
+ lines,
73
+ wrap,
74
+ }: {
75
+ lines: DiffLine[];
76
+ // Text wraps; source scrolls sideways.
77
+ wrap: boolean;
78
+ }) {
79
+ const [all, setAll] = useState(false);
80
+ const shown = useMemo(
81
+ () => (all ? lines : foldUnchanged(lines)),
82
+ [all, lines]
83
+ );
84
+ const folded = shown.some((s) => "gap" in s);
85
+ return (
86
+ <div className="flex flex-col">
87
+ <div className={cn("py-2", !wrap && "overflow-x-auto")}>
88
+ <div className={cn(!wrap && "w-max min-w-full")}>
89
+ {shown.map((s, i) =>
90
+ "gap" in s ? (
91
+ <button
92
+ key={i}
93
+ type="button"
94
+ onClick={() => setAll(true)}
95
+ className="text-muted-foreground hover:text-foreground bg-muted/50 my-1 flex min-h-11 w-full items-center justify-center text-xs md:min-h-7"
96
+ >
97
+ {s.gap} unchanged {s.gap === 1 ? "line" : "lines"} · show all
98
+ </button>
99
+ ) : (
100
+ <Row key={i} line={s} wrap={wrap} />
101
+ )
102
+ )}
103
+ </div>
104
+ </div>
105
+ {all && folded === false && lines.some((l) => l.t !== "eq") && (
106
+ <button
107
+ type="button"
108
+ onClick={() => setAll(false)}
109
+ className="text-muted-foreground hover:text-foreground min-h-11 text-xs md:min-h-9"
110
+ >
111
+ Fold unchanged lines
112
+ </button>
113
+ )}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Segmented } from "@/components/ui/segmented";
5
+ import { ArtifactFrame } from "@/components/Chat/ArtifactFrame";
6
+ import type { ArtifactVersion } from "@/lib/artifacts/actions";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ // Two versions in the viewer's sandboxed frames: next to each other when
10
+ // there's room, otherwise one at a time with a toggle.
11
+ export function SideBySide({
12
+ from,
13
+ to,
14
+ }: {
15
+ from: ArtifactVersion;
16
+ to: ArtifactVersion;
17
+ }) {
18
+ const [side, setSide] = useState<"from" | "to">("to");
19
+ const panes = [
20
+ { key: "from", v: from },
21
+ { key: "to", v: to },
22
+ ] as const;
23
+ return (
24
+ <div className="flex h-full min-h-[60dvh] flex-col gap-2 p-2 @2xl:p-3">
25
+ <div className="@2xl:hidden">
26
+ <Segmented
27
+ label="Version shown"
28
+ value={side}
29
+ onChange={setSide}
30
+ options={panes.map((p) => ({
31
+ value: p.key,
32
+ label: `v${p.v.version}${p.key === "from" ? " (before)" : " (after)"}`,
33
+ }))}
34
+ />
35
+ </div>
36
+ <div className="grid min-h-0 flex-1 gap-3 @2xl:grid-cols-2">
37
+ {panes.map(({ key, v }) => (
38
+ <div
39
+ key={key}
40
+ className={cn(
41
+ "min-h-0 flex-col gap-1",
42
+ side === key ? "flex" : "hidden @2xl:flex"
43
+ )}
44
+ >
45
+ <div className="text-muted-foreground hidden px-1 text-xs @2xl:block">
46
+ v{v.version}
47
+ {key === "from" ? " · before" : " · after"}
48
+ </div>
49
+ <ArtifactFrame
50
+ artifactId={v.id}
51
+ title={`${v.title} v${v.version}`}
52
+ fill
53
+ className="bg-background min-h-[50dvh] rounded-lg shadow-sm"
54
+ />
55
+ </div>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "@/components/ui/select";
10
+ import type { ArtifactVersion } from "@/lib/artifacts/actions";
11
+
12
+ const label = (v: ArtifactVersion, latest: boolean) =>
13
+ `v${v.version}${latest ? " · latest" : ""}`;
14
+
15
+ export function VersionPicker({
16
+ name,
17
+ value,
18
+ versions,
19
+ onChange,
20
+ }: {
21
+ name: string;
22
+ value: string;
23
+ versions: ArtifactVersion[];
24
+ onChange: (id: string) => void;
25
+ }) {
26
+ const last = versions[versions.length - 1];
27
+ return (
28
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
29
+ <span className="text-muted-foreground text-[11px] font-medium tracking-wider uppercase">
30
+ {name}
31
+ </span>
32
+ <Select value={value} onValueChange={onChange}>
33
+ <SelectTrigger aria-label={`${name} version`} className="h-11 md:h-9">
34
+ <SelectValue />
35
+ </SelectTrigger>
36
+ <SelectContent>
37
+ {[...versions].reverse().map((v) => (
38
+ <SelectItem key={v.id} value={v.id} className="min-h-11 md:min-h-8">
39
+ {label(v, v.id === last?.id)}
40
+ {v.note ? ` · ${v.note}` : ""}
41
+ </SelectItem>
42
+ ))}
43
+ </SelectContent>
44
+ </Select>
45
+ </div>
46
+ );
47
+ }