@saadnvd1/agent-os 0.3.566 → 0.3.567
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/components/Share/ShareDialog.tsx +16 -26
- package/data/shares/queries.ts +19 -56
- package/lib/share/view.ts +123 -0
- package/package.json +1 -1
- package/components/Share/format.test.ts +0 -20
- package/components/Share/format.ts +0 -33
|
@@ -27,24 +27,17 @@ import {
|
|
|
27
27
|
type SharePreview,
|
|
28
28
|
} from "@/data/shares";
|
|
29
29
|
import { shareUi, shareUiActions } from "@/stores/shareUi";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
assistant: "Agent",
|
|
42
|
-
plan: "Plan",
|
|
43
|
-
error: "Error",
|
|
44
|
-
divider: "Divider",
|
|
45
|
-
artifact: "Page",
|
|
46
|
-
detail: "Tool",
|
|
47
|
-
};
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_EXPIRY,
|
|
32
|
+
EXPIRIES,
|
|
33
|
+
EXPIRY_LABEL,
|
|
34
|
+
TYPE_LABEL,
|
|
35
|
+
fileName,
|
|
36
|
+
formatBytes,
|
|
37
|
+
nothingLeft,
|
|
38
|
+
redactionSummary,
|
|
39
|
+
toggleDrop,
|
|
40
|
+
} from "@/lib/share/view";
|
|
48
41
|
|
|
49
42
|
// Opened from a menu or an artifact: the redacted page exactly as it would be
|
|
50
43
|
// uploaded, with what to leave out, then Create link or Download HTML.
|
|
@@ -77,7 +70,7 @@ function ShareBody({
|
|
|
77
70
|
}) {
|
|
78
71
|
const [tools, setTools] = useState(false);
|
|
79
72
|
const [dropped, setDropped] = useState<string[]>([]);
|
|
80
|
-
const [expiresIn, setExpiresIn] = useState<ShareExpiry>(
|
|
73
|
+
const [expiresIn, setExpiresIn] = useState<ShareExpiry>(DEFAULT_EXPIRY);
|
|
81
74
|
const [preview, setPreview] = useState<SharePreview | null>(null);
|
|
82
75
|
const [error, setError] = useState<string | null>(null);
|
|
83
76
|
const [created, setCreated] = useState<string | null>(null);
|
|
@@ -111,10 +104,7 @@ function ShareBody({
|
|
|
111
104
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
105
|
}, [kind, sessionId, artifactId, tools, dropped]);
|
|
113
106
|
|
|
114
|
-
const toggle = (id: string) =>
|
|
115
|
-
setDropped((d) =>
|
|
116
|
-
d.includes(id) ? d.filter((x) => x !== id) : [...d, id]
|
|
117
|
-
);
|
|
107
|
+
const toggle = (id: string) => setDropped((d) => toggleDrop(d, id));
|
|
118
108
|
|
|
119
109
|
const makeLink = async () => {
|
|
120
110
|
if (!preview) return;
|
|
@@ -166,7 +156,7 @@ function ShareBody({
|
|
|
166
156
|
|
|
167
157
|
const linked = preview?.connectLinked ?? false;
|
|
168
158
|
const empty =
|
|
169
|
-
kind === "chat" && !!preview && preview.items
|
|
159
|
+
kind === "chat" && !!preview && nothingLeft(preview.items, dropped);
|
|
170
160
|
const summary = preview ? redactionSummary(preview.hits) : "";
|
|
171
161
|
|
|
172
162
|
return (
|
|
@@ -245,8 +235,8 @@ function ShareBody({
|
|
|
245
235
|
</SelectTrigger>
|
|
246
236
|
<SelectContent>
|
|
247
237
|
{EXPIRIES.map((e) => (
|
|
248
|
-
<SelectItem key={e
|
|
249
|
-
{e
|
|
238
|
+
<SelectItem key={e} value={e}>
|
|
239
|
+
{EXPIRY_LABEL[e]}
|
|
250
240
|
</SelectItem>
|
|
251
241
|
))}
|
|
252
242
|
</SelectContent>
|
package/data/shares/queries.ts
CHANGED
|
@@ -1,45 +1,30 @@
|
|
|
1
1
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type {
|
|
3
|
+
CreatedShare,
|
|
4
|
+
PreviewRequest,
|
|
5
|
+
SharePreview,
|
|
6
|
+
SharesResponse,
|
|
7
|
+
ShareExpiry,
|
|
8
|
+
} from "@/lib/share/view";
|
|
2
9
|
import { shareKeys } from "./keys";
|
|
3
10
|
|
|
11
|
+
export type {
|
|
12
|
+
CreatedShare,
|
|
13
|
+
PreviewItem,
|
|
14
|
+
PreviewRequest,
|
|
15
|
+
SharedLinkRow,
|
|
16
|
+
SharePreview,
|
|
17
|
+
SharesResponse,
|
|
18
|
+
ShareExpiry,
|
|
19
|
+
ShareTarget,
|
|
20
|
+
} from "@/lib/share/view";
|
|
21
|
+
|
|
4
22
|
async function json<T>(res: Response): Promise<T> {
|
|
5
23
|
const data = await res.json().catch(() => ({}));
|
|
6
24
|
if (!res.ok) throw new Error(data.error || "Request failed");
|
|
7
25
|
return data;
|
|
8
26
|
}
|
|
9
27
|
|
|
10
|
-
export type ShareExpiry = "1d" | "7d" | "30d" | "never";
|
|
11
|
-
|
|
12
|
-
export interface ShareTarget {
|
|
13
|
-
kind: "chat" | "artifact";
|
|
14
|
-
sessionId?: string;
|
|
15
|
-
artifactId?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PreviewRequest extends ShareTarget {
|
|
19
|
-
includeTools: boolean;
|
|
20
|
-
/** Messages left out of the share. */
|
|
21
|
-
dropIds?: string[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface PreviewItem {
|
|
25
|
-
id: string;
|
|
26
|
-
type: string;
|
|
27
|
-
tools: boolean;
|
|
28
|
-
excerpt: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface SharePreview {
|
|
32
|
-
html: string;
|
|
33
|
-
title: string;
|
|
34
|
-
bytes: number;
|
|
35
|
-
hash: string;
|
|
36
|
-
sharedAt: string;
|
|
37
|
-
items: PreviewItem[];
|
|
38
|
-
hits: Record<"secret" | "path" | "ip" | "host" | "email", number>;
|
|
39
|
-
tooBig: boolean;
|
|
40
|
-
connectLinked: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
28
|
const post = (url: string, body: unknown) =>
|
|
44
29
|
fetch(url, {
|
|
45
30
|
method: "POST",
|
|
@@ -64,33 +49,11 @@ export function useCreateShare() {
|
|
|
64
49
|
hash: string;
|
|
65
50
|
expiresIn: ShareExpiry;
|
|
66
51
|
}
|
|
67
|
-
) =>
|
|
68
|
-
json<{ id: string; url: string; expiresAt: string | null }>(
|
|
69
|
-
await post("/api/shares", req)
|
|
70
|
-
),
|
|
52
|
+
) => json<CreatedShare>(await post("/api/shares", req)),
|
|
71
53
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: shareKeys.all }),
|
|
72
54
|
});
|
|
73
55
|
}
|
|
74
56
|
|
|
75
|
-
export interface SharedLinkRow {
|
|
76
|
-
id: string;
|
|
77
|
-
url: string;
|
|
78
|
-
kind: "chat" | "artifact";
|
|
79
|
-
title: string;
|
|
80
|
-
sessionId: string | null;
|
|
81
|
-
artifactId: string | null;
|
|
82
|
-
createdAt: string;
|
|
83
|
-
expiresAt: string | null;
|
|
84
|
-
status: "active" | "expired" | "revoked";
|
|
85
|
-
views: number | null;
|
|
86
|
-
lastViewedAt: string | null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface SharesResponse {
|
|
90
|
-
connect: "ok" | "unavailable" | "unlinked";
|
|
91
|
-
shares: SharedLinkRow[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
57
|
export function useSharesQuery(enabled = true) {
|
|
95
58
|
return useQuery({
|
|
96
59
|
queryKey: shareKeys.list(),
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Pure share logic: wire types and helpers. Imports nothing outside itself so
|
|
2
|
+
// the native app can sync it (scripts/sync-shared).
|
|
3
|
+
|
|
4
|
+
export type ShareExpiry = "1d" | "7d" | "30d" | "never";
|
|
5
|
+
|
|
6
|
+
export interface ShareTarget {
|
|
7
|
+
kind: "chat" | "artifact";
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
artifactId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PreviewRequest extends ShareTarget {
|
|
13
|
+
includeTools: boolean;
|
|
14
|
+
/** Messages left out of the share. */
|
|
15
|
+
dropIds?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PreviewItem {
|
|
19
|
+
id: string;
|
|
20
|
+
type: string;
|
|
21
|
+
tools: boolean;
|
|
22
|
+
excerpt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SharePreview {
|
|
26
|
+
html: string;
|
|
27
|
+
title: string;
|
|
28
|
+
bytes: number;
|
|
29
|
+
hash: string;
|
|
30
|
+
sharedAt: string;
|
|
31
|
+
items: PreviewItem[];
|
|
32
|
+
hits: Record<"secret" | "path" | "ip" | "host" | "email", number>;
|
|
33
|
+
tooBig: boolean;
|
|
34
|
+
connectLinked: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SharedLinkRow {
|
|
38
|
+
id: string;
|
|
39
|
+
url: string;
|
|
40
|
+
kind: "chat" | "artifact";
|
|
41
|
+
title: string;
|
|
42
|
+
sessionId: string | null;
|
|
43
|
+
artifactId: string | null;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
expiresAt: string | null;
|
|
46
|
+
status: "active" | "expired" | "revoked";
|
|
47
|
+
views: number | null;
|
|
48
|
+
lastViewedAt: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SharesResponse {
|
|
52
|
+
connect: "ok" | "unavailable" | "unlinked";
|
|
53
|
+
shares: SharedLinkRow[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CreatedShare {
|
|
57
|
+
id: string;
|
|
58
|
+
url: string;
|
|
59
|
+
expiresAt: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const EXPIRIES: ShareExpiry[] = ["1d", "7d", "30d", "never"];
|
|
63
|
+
export const DEFAULT_EXPIRY: ShareExpiry = "7d";
|
|
64
|
+
|
|
65
|
+
export const EXPIRY_LABEL: Record<ShareExpiry, string> = {
|
|
66
|
+
"1d": "1 day",
|
|
67
|
+
"7d": "7 days",
|
|
68
|
+
"30d": "30 days",
|
|
69
|
+
never: "Never",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const TYPE_LABEL: Record<string, string> = {
|
|
73
|
+
user: "You",
|
|
74
|
+
assistant: "Agent",
|
|
75
|
+
plan: "Plan",
|
|
76
|
+
error: "Error",
|
|
77
|
+
divider: "Divider",
|
|
78
|
+
artifact: "Page",
|
|
79
|
+
detail: "Tool",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const PLURAL: Record<keyof SharePreview["hits"], [string, string]> = {
|
|
83
|
+
secret: ["secret", "secrets"],
|
|
84
|
+
path: ["path", "paths"],
|
|
85
|
+
ip: ["address", "addresses"],
|
|
86
|
+
host: ["host", "hosts"],
|
|
87
|
+
email: ["email", "emails"],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** "Masked: 3 secrets, 12 paths", or "" when nothing was. */
|
|
91
|
+
export function redactionSummary(hits: SharePreview["hits"]): string {
|
|
92
|
+
const parts = (Object.keys(PLURAL) as (keyof typeof PLURAL)[])
|
|
93
|
+
.filter((k) => hits[k] > 0)
|
|
94
|
+
.map((k) => `${hits[k]} ${PLURAL[k][hits[k] === 1 ? 0 : 1]}`);
|
|
95
|
+
return parts.length ? `Masked: ${parts.join(", ")}` : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatBytes(n: number): string {
|
|
99
|
+
if (n < 1024) return `${n} bytes`;
|
|
100
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
101
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A file name for a download: the title, plain, with .html. */
|
|
105
|
+
export function fileName(title: string): string {
|
|
106
|
+
const slug = title
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
109
|
+
.replace(/^-+|-+$/g, "")
|
|
110
|
+
.slice(0, 60);
|
|
111
|
+
return `${slug || "shared"}.html`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function toggleDrop(dropped: string[], id: string): string[] {
|
|
115
|
+
return dropped.includes(id)
|
|
116
|
+
? dropped.filter((x) => x !== id)
|
|
117
|
+
: [...dropped, id];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** True when every listed item is dropped. Ids of items no longer listed (tool rows hidden by turning tools off) don't count. */
|
|
121
|
+
export function nothingLeft(items: PreviewItem[], dropped: string[]): boolean {
|
|
122
|
+
return items.every((i) => dropped.includes(i.id));
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { fileName, formatBytes, redactionSummary } from "./format";
|
|
3
|
-
|
|
4
|
-
describe("share format", () => {
|
|
5
|
-
it("summarises what was masked, singular and plural", () => {
|
|
6
|
-
const none = { secret: 0, path: 0, ip: 0, host: 0, email: 0 };
|
|
7
|
-
expect(redactionSummary(none)).toBe("");
|
|
8
|
-
expect(redactionSummary({ ...none, secret: 1, path: 12 })).toBe(
|
|
9
|
-
"Masked: 1 secret, 12 paths"
|
|
10
|
-
);
|
|
11
|
-
});
|
|
12
|
-
it("names the download after the title", () => {
|
|
13
|
-
expect(fileName("My chat: part 2!")).toBe("my-chat-part-2.html");
|
|
14
|
-
expect(fileName("???")).toBe("shared.html");
|
|
15
|
-
});
|
|
16
|
-
it("sizes", () => {
|
|
17
|
-
expect(formatBytes(500)).toBe("500 bytes");
|
|
18
|
-
expect(formatBytes(2048)).toBe("2.0 KB");
|
|
19
|
-
});
|
|
20
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { SharePreview } from "@/data/shares";
|
|
2
|
-
|
|
3
|
-
const PLURAL: Record<keyof SharePreview["hits"], [string, string]> = {
|
|
4
|
-
secret: ["secret", "secrets"],
|
|
5
|
-
path: ["path", "paths"],
|
|
6
|
-
ip: ["address", "addresses"],
|
|
7
|
-
host: ["host", "hosts"],
|
|
8
|
-
email: ["email", "emails"],
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/** "Masked: 3 secrets, 12 paths", or "" when nothing was. */
|
|
12
|
-
export function redactionSummary(hits: SharePreview["hits"]): string {
|
|
13
|
-
const parts = (Object.keys(PLURAL) as (keyof typeof PLURAL)[])
|
|
14
|
-
.filter((k) => hits[k] > 0)
|
|
15
|
-
.map((k) => `${hits[k]} ${PLURAL[k][hits[k] === 1 ? 0 : 1]}`);
|
|
16
|
-
return parts.length ? `Masked: ${parts.join(", ")}` : "";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function formatBytes(n: number): string {
|
|
20
|
-
if (n < 1024) return `${n} bytes`;
|
|
21
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
22
|
-
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** A file name for a download: the title, plain, with .html. */
|
|
26
|
-
export function fileName(title: string): string {
|
|
27
|
-
const slug = title
|
|
28
|
-
.toLowerCase()
|
|
29
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
30
|
-
.replace(/^-+|-+$/g, "")
|
|
31
|
-
.slice(0, 60);
|
|
32
|
-
return `${slug || "shared"}.html`;
|
|
33
|
-
}
|