@saadnvd1/agent-os 0.3.560 → 0.3.561
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/README.md +1 -0
- package/app/api/shares/[id]/route.ts +23 -0
- package/app/api/shares/preview/route.ts +30 -0
- package/app/api/shares/route.test.ts +455 -0
- package/app/api/shares/route.ts +101 -0
- package/app/page.tsx +5 -0
- package/components/Chat/Artifact.tsx +17 -1
- package/components/Pane/MobileTabBar.tsx +19 -0
- package/components/Pane/PaneMenu.tsx +16 -0
- package/components/SessionList/SessionRowMenu.tsx +16 -0
- package/components/Settings/SettingsDialog.test.ts +3 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Settings/commands.ts +9 -0
- package/components/Settings/entry-points.test.ts +1 -0
- package/components/Share/ShareDialog.tsx +336 -0
- package/components/Share/format.test.ts +20 -0
- package/components/Share/format.ts +33 -0
- package/components/Shares/SharedLinksPanel.tsx +147 -0
- package/data/shares/index.ts +2 -0
- package/data/shares/keys.ts +4 -0
- package/data/shares/queries.ts +111 -0
- package/lib/chat/drivers/claude.ts +5 -4
- package/lib/connect/control-api.ts +84 -0
- package/lib/db/migrations.ts +23 -0
- package/lib/orchestrator/untrusted.ts +12 -2
- package/lib/security/demo.ts +1 -0
- package/lib/session-audit/normalize.ts +1 -1
- package/lib/settings-url.ts +1 -0
- package/lib/share/connect.ts +86 -0
- package/lib/share/context.ts +50 -0
- package/lib/share/redact.ts +190 -0
- package/lib/share/render.tsx +161 -0
- package/lib/share/route.ts +33 -0
- package/lib/share/service.ts +159 -0
- package/lib/share/snapshot.ts +261 -0
- package/lib/share/static-markup.ts +16 -0
- package/lib/share/store.ts +39 -0
- package/lib/share/styles.ts +83 -0
- package/package.json +1 -1
- package/stores/shareUi.ts +17 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a share contains: a chat's items, or one artifact, already redacted.
|
|
3
|
+
* Nothing here leaves the machine; the renderer turns it into a page.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { listItems } from "../chat/store";
|
|
7
|
+
import type { ChatItem } from "../chat/events";
|
|
8
|
+
import { getSession } from "../chat/registry";
|
|
9
|
+
import { getArtifact, readArtifactHtml } from "../artifacts/store";
|
|
10
|
+
import { isSecretPath } from "../secret-paths";
|
|
11
|
+
import {
|
|
12
|
+
addHits,
|
|
13
|
+
noHits,
|
|
14
|
+
redactForShare,
|
|
15
|
+
type Hits,
|
|
16
|
+
type ShareContext,
|
|
17
|
+
} from "./redact";
|
|
18
|
+
|
|
19
|
+
export type SnapshotType =
|
|
20
|
+
| "user"
|
|
21
|
+
| "assistant"
|
|
22
|
+
| "plan"
|
|
23
|
+
| "error"
|
|
24
|
+
| "divider"
|
|
25
|
+
| "artifact"
|
|
26
|
+
| "detail";
|
|
27
|
+
|
|
28
|
+
export interface SnapshotItem {
|
|
29
|
+
id: string;
|
|
30
|
+
type: SnapshotType;
|
|
31
|
+
/** For a detail row: what it is (a tool's title, "Thinking"). */
|
|
32
|
+
label?: string;
|
|
33
|
+
text: string;
|
|
34
|
+
/** An artifact's page. */
|
|
35
|
+
html?: string;
|
|
36
|
+
height?: number;
|
|
37
|
+
/** The first words, for the dialog's message list. */
|
|
38
|
+
excerpt: string;
|
|
39
|
+
/** Hidden unless "include tool calls and thinking" is on. */
|
|
40
|
+
tools: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Snapshot {
|
|
44
|
+
title: string;
|
|
45
|
+
items: SnapshotItem[];
|
|
46
|
+
/** Whether the tools group was left out, to say so on the page. */
|
|
47
|
+
toolsHidden: boolean;
|
|
48
|
+
hits: Hits;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const MAX_TOOL_INPUT = 4_000;
|
|
52
|
+
export const MAX_TOOL_OUTPUT = 20_000;
|
|
53
|
+
const HIDDEN_FILE = "[contents of a secret file hidden]";
|
|
54
|
+
const NO_IMAGE = "[image not shared]";
|
|
55
|
+
|
|
56
|
+
const clip = (s: string, max: number) =>
|
|
57
|
+
s.length > max
|
|
58
|
+
? `${s.slice(0, max)}\n… (${s.length - max} more characters)`
|
|
59
|
+
: s;
|
|
60
|
+
|
|
61
|
+
const excerptOf = (s: string) => s.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
62
|
+
|
|
63
|
+
// Every string in a tool's input, to see whether it names a secret file.
|
|
64
|
+
function strings(v: unknown, out: string[] = []): string[] {
|
|
65
|
+
if (typeof v === "string") out.push(v);
|
|
66
|
+
else if (Array.isArray(v)) v.forEach((x) => strings(x, out));
|
|
67
|
+
else if (v && typeof v === "object")
|
|
68
|
+
Object.values(v).forEach((x) => strings(x, out));
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const json = (v: unknown) => {
|
|
73
|
+
try {
|
|
74
|
+
return typeof v === "string" ? v : (JSON.stringify(v, null, 2) ?? "");
|
|
75
|
+
} catch {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Items as the reader sees them: what an undo took back is gone. */
|
|
81
|
+
export function visible(items: ChatItem[]): ChatItem[] {
|
|
82
|
+
const out: ChatItem[] = [];
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
if (item.kind !== "undo") {
|
|
85
|
+
out.push(item);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const at = out.findIndex((i) => i.id === item.from);
|
|
89
|
+
if (at !== -1) out.length = at;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type Raw = Omit<SnapshotItem, "excerpt">;
|
|
95
|
+
|
|
96
|
+
/** One chat item as a row, or null for what is never shared. */
|
|
97
|
+
export function toRaw(item: ChatItem): Raw | null {
|
|
98
|
+
const id = item.id;
|
|
99
|
+
switch (item.kind) {
|
|
100
|
+
case "user": {
|
|
101
|
+
const images = item.images?.length
|
|
102
|
+
? "\n" + item.images.map(() => NO_IMAGE).join("\n")
|
|
103
|
+
: "";
|
|
104
|
+
return { id, type: "user", text: item.text + images, tools: false };
|
|
105
|
+
}
|
|
106
|
+
case "assistant":
|
|
107
|
+
return item.text.trim()
|
|
108
|
+
? { id, type: "assistant", text: item.text, tools: false }
|
|
109
|
+
: null;
|
|
110
|
+
case "plan":
|
|
111
|
+
return { id, type: "plan", text: item.plan, tools: false };
|
|
112
|
+
case "error":
|
|
113
|
+
return { id, type: "error", text: item.message, tools: false };
|
|
114
|
+
case "compacted":
|
|
115
|
+
return {
|
|
116
|
+
id,
|
|
117
|
+
type: "divider",
|
|
118
|
+
text: "Conversation compacted",
|
|
119
|
+
tools: false,
|
|
120
|
+
};
|
|
121
|
+
case "artifact":
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
type: "artifact",
|
|
125
|
+
label: item.title,
|
|
126
|
+
text: "",
|
|
127
|
+
height: item.height,
|
|
128
|
+
tools: false,
|
|
129
|
+
};
|
|
130
|
+
case "reasoning":
|
|
131
|
+
return item.text.trim()
|
|
132
|
+
? {
|
|
133
|
+
id,
|
|
134
|
+
type: "detail",
|
|
135
|
+
label: "Thinking",
|
|
136
|
+
text: item.text,
|
|
137
|
+
tools: true,
|
|
138
|
+
}
|
|
139
|
+
: null;
|
|
140
|
+
case "tool": {
|
|
141
|
+
const secret = strings(item.input).some(isSecretPath);
|
|
142
|
+
const input = clip(json(item.input), MAX_TOOL_INPUT);
|
|
143
|
+
const output = secret
|
|
144
|
+
? HIDDEN_FILE
|
|
145
|
+
: clip(item.output ?? "", MAX_TOOL_OUTPUT);
|
|
146
|
+
return {
|
|
147
|
+
id,
|
|
148
|
+
type: "detail",
|
|
149
|
+
label: item.title || item.name,
|
|
150
|
+
text: output ? `${input}\n\n${output}` : input,
|
|
151
|
+
tools: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "command_output":
|
|
155
|
+
return {
|
|
156
|
+
id,
|
|
157
|
+
type: "detail",
|
|
158
|
+
label: "Command output",
|
|
159
|
+
text: clip(item.text, MAX_TOOL_OUTPUT),
|
|
160
|
+
tools: true,
|
|
161
|
+
};
|
|
162
|
+
case "todos":
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
type: "detail",
|
|
166
|
+
label: "To do",
|
|
167
|
+
text: item.todos
|
|
168
|
+
.map((t) => `${t.status === "completed" ? "[x]" : "[ ]"} ${t.text}`)
|
|
169
|
+
.join("\n"),
|
|
170
|
+
tools: true,
|
|
171
|
+
};
|
|
172
|
+
case "mcp":
|
|
173
|
+
return {
|
|
174
|
+
id,
|
|
175
|
+
type: "detail",
|
|
176
|
+
label: "MCP servers",
|
|
177
|
+
text: item.servers.map((s) => `${s.name}: ${s.status}`).join("\n"),
|
|
178
|
+
tools: true,
|
|
179
|
+
};
|
|
180
|
+
case "task":
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
type: "detail",
|
|
184
|
+
label: item.description,
|
|
185
|
+
text: item.summary ?? item.status,
|
|
186
|
+
tools: true,
|
|
187
|
+
};
|
|
188
|
+
default:
|
|
189
|
+
// approval, note, undo, turn_end
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** What the preview lists: every shareable item, whether or not it's on. */
|
|
195
|
+
export async function buildChatSnapshot(
|
|
196
|
+
sessionId: string,
|
|
197
|
+
ctx: ShareContext,
|
|
198
|
+
opts: { includeTools: boolean; dropIds?: string[] }
|
|
199
|
+
): Promise<Snapshot> {
|
|
200
|
+
const session = getSession(sessionId);
|
|
201
|
+
const dropped = new Set(opts.dropIds ?? []);
|
|
202
|
+
let hits = noHits();
|
|
203
|
+
const take = (text: string) => {
|
|
204
|
+
const r = redactForShare(text, ctx);
|
|
205
|
+
hits = addHits(hits, r.hits);
|
|
206
|
+
return r.text;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const items: SnapshotItem[] = [];
|
|
210
|
+
for (const item of visible(listItems(sessionId))) {
|
|
211
|
+
const raw = toRaw(item);
|
|
212
|
+
if (!raw) continue;
|
|
213
|
+
if (raw.tools && !opts.includeTools) continue;
|
|
214
|
+
if (dropped.has(raw.id)) continue;
|
|
215
|
+
const row: SnapshotItem = {
|
|
216
|
+
...raw,
|
|
217
|
+
label: raw.label === undefined ? undefined : take(raw.label),
|
|
218
|
+
text: take(raw.text),
|
|
219
|
+
excerpt: "",
|
|
220
|
+
};
|
|
221
|
+
if (raw.type === "artifact") {
|
|
222
|
+
const artifact = getArtifact(listArtifactId(item) ?? "");
|
|
223
|
+
const html = artifact ? await readArtifactHtml(artifact) : null;
|
|
224
|
+
if (html === null) continue;
|
|
225
|
+
row.html = take(html);
|
|
226
|
+
}
|
|
227
|
+
row.excerpt = excerptOf(row.text || row.label || "");
|
|
228
|
+
items.push(row);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
title: take(session.name).slice(0, 120) || "Chat",
|
|
232
|
+
items,
|
|
233
|
+
toolsHidden: !opts.includeTools,
|
|
234
|
+
hits,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const listArtifactId = (item: ChatItem) =>
|
|
239
|
+
item.kind === "artifact" ? item.artifactId : null;
|
|
240
|
+
|
|
241
|
+
export interface ArtifactSnapshot {
|
|
242
|
+
title: string;
|
|
243
|
+
html: string;
|
|
244
|
+
hits: Hits;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function buildArtifactSnapshot(
|
|
248
|
+
artifactId: string,
|
|
249
|
+
ctx: ShareContext
|
|
250
|
+
): Promise<ArtifactSnapshot | null> {
|
|
251
|
+
const artifact = getArtifact(artifactId);
|
|
252
|
+
const html = artifact ? await readArtifactHtml(artifact) : null;
|
|
253
|
+
if (!artifact || html === null) return null;
|
|
254
|
+
const page = redactForShare(html, ctx);
|
|
255
|
+
const title = redactForShare(artifact.title, ctx);
|
|
256
|
+
return {
|
|
257
|
+
title: title.text.slice(0, 120) || "Page",
|
|
258
|
+
html: page.text,
|
|
259
|
+
hits: addHits(page.hits, title.hits),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
// Next refuses a static import of react-dom/server in a route's module graph
|
|
6
|
+
// (it is meant for pages). A snapshot is a plain string, not a page, so it is
|
|
7
|
+
// loaded by name at run time; the bundler never sees the import.
|
|
8
|
+
const load = createRequire(path.join(process.cwd(), "package.json"));
|
|
9
|
+
let render: ((el: ReactElement) => string) | null = null;
|
|
10
|
+
|
|
11
|
+
export function renderToStaticMarkup(element: ReactElement): string {
|
|
12
|
+
render ??= (
|
|
13
|
+
load("react-dom/server") as { renderToStaticMarkup: typeof render & object }
|
|
14
|
+
).renderToStaticMarkup;
|
|
15
|
+
return render!(element);
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { db } from "../db";
|
|
2
|
+
|
|
3
|
+
export interface SharedLink {
|
|
4
|
+
id: string;
|
|
5
|
+
url: string;
|
|
6
|
+
kind: "chat" | "artifact";
|
|
7
|
+
title: string;
|
|
8
|
+
session_id: string | null;
|
|
9
|
+
artifact_id: string | null;
|
|
10
|
+
expires_at: string | null;
|
|
11
|
+
created_at: string;
|
|
12
|
+
revoked_at: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function insertSharedLink(link: SharedLink): void {
|
|
16
|
+
db.prepare(
|
|
17
|
+
`INSERT INTO shared_links
|
|
18
|
+
(id, url, kind, title, session_id, artifact_id, expires_at, created_at, revoked_at)
|
|
19
|
+
VALUES (@id, @url, @kind, @title, @session_id, @artifact_id, @expires_at, @created_at, @revoked_at)`
|
|
20
|
+
).run(link);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function listSharedLinks(): SharedLink[] {
|
|
24
|
+
return db
|
|
25
|
+
.prepare(`SELECT * FROM shared_links ORDER BY created_at DESC, rowid DESC`)
|
|
26
|
+
.all() as SharedLink[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getSharedLink(id: string): SharedLink | undefined {
|
|
30
|
+
return db.prepare(`SELECT * FROM shared_links WHERE id = ?`).get(id) as
|
|
31
|
+
| SharedLink
|
|
32
|
+
| undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function markRevoked(id: string): void {
|
|
36
|
+
db.prepare(
|
|
37
|
+
`UPDATE shared_links SET revoked_at = COALESCE(revoked_at, ?) WHERE id = ?`
|
|
38
|
+
).run(new Date().toISOString(), id);
|
|
39
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stylesheet inlined into a shared page. The colours are the app's
|
|
3
|
+
* default light and dark themes (styles/themes.css, checked by a test), under
|
|
4
|
+
* prefers-color-scheme, so a snapshot looks like the chat it came from.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const LIGHT = {
|
|
8
|
+
background: "240 10% 95.5%",
|
|
9
|
+
foreground: "240 12% 14%",
|
|
10
|
+
card: "0 0% 100%",
|
|
11
|
+
primary: "262 83% 58%",
|
|
12
|
+
"primary-foreground": "0 0% 100%",
|
|
13
|
+
muted: "240 16% 94%",
|
|
14
|
+
"muted-foreground": "240 6% 46%",
|
|
15
|
+
border: "240 14% 88%",
|
|
16
|
+
destructive: "352 80% 56%",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const DARK = {
|
|
20
|
+
background: "240 18% 3.5%",
|
|
21
|
+
foreground: "240 10% 93%",
|
|
22
|
+
card: "240 14% 6%",
|
|
23
|
+
primary: "262 92% 68%",
|
|
24
|
+
"primary-foreground": "0 0% 100%",
|
|
25
|
+
muted: "240 12% 10%",
|
|
26
|
+
"muted-foreground": "240 6% 58%",
|
|
27
|
+
border: "240 10% 15%",
|
|
28
|
+
destructive: "352 85% 62%",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const vars = (v: Record<string, string>) =>
|
|
32
|
+
Object.entries(v)
|
|
33
|
+
.map(([k, val]) => `--${k}:${val};`)
|
|
34
|
+
.join("");
|
|
35
|
+
|
|
36
|
+
// Syntax colours, One Light and One Dark as the chat uses.
|
|
37
|
+
const TOKENS_LIGHT = `.token.comment,.token.prolog,.token.cdata{color:#a0a1a7;font-style:italic}.token.keyword,.token.selector,.token.atrule{color:#a626a4}.token.string,.token.char,.token.attr-value,.token.inserted{color:#50a14f}.token.number,.token.boolean,.token.constant,.token.symbol{color:#986801}.token.function,.token.class-name{color:#4078f2}.token.tag,.token.property,.token.deleted{color:#e45649}.token.operator,.token.punctuation{color:#383a42}.token.builtin,.token.attr-name{color:#c18401}`;
|
|
38
|
+
const TOKENS_DARK = `.token.comment,.token.prolog,.token.cdata{color:#5c6370;font-style:italic}.token.keyword,.token.selector,.token.atrule{color:#c678dd}.token.string,.token.char,.token.attr-value,.token.inserted{color:#98c379}.token.number,.token.boolean,.token.constant,.token.symbol{color:#d19a66}.token.function,.token.class-name{color:#61afef}.token.tag,.token.property,.token.deleted{color:#e06c75}.token.operator,.token.punctuation{color:#abb2bf}.token.builtin,.token.attr-name{color:#e5c07b}`;
|
|
39
|
+
|
|
40
|
+
export const STYLES = `
|
|
41
|
+
:root{color-scheme:light dark;${vars(LIGHT)}}
|
|
42
|
+
${TOKENS_LIGHT}
|
|
43
|
+
@media (prefers-color-scheme:dark){:root{${vars(DARK)}}${TOKENS_DARK}}
|
|
44
|
+
*{box-sizing:border-box}
|
|
45
|
+
html{-webkit-text-size-adjust:100%}
|
|
46
|
+
body{margin:0;background:hsl(var(--background));color:hsl(var(--foreground));font:15px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
47
|
+
.wrap{max-width:760px;margin:0 auto;padding:24px 16px 64px}
|
|
48
|
+
header{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid hsl(var(--border))}
|
|
49
|
+
h1.title{margin:0 0 4px;font-size:20px;line-height:1.3}
|
|
50
|
+
.meta{margin:0;color:hsl(var(--muted-foreground));font-size:13px}
|
|
51
|
+
.msg{margin:16px 0}
|
|
52
|
+
.user{display:flex;justify-content:flex-end}
|
|
53
|
+
.user .bubble{max-width:85%;background:hsl(var(--muted));border-radius:14px;padding:8px 14px;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
54
|
+
.assistant,.plan{overflow-wrap:anywhere}
|
|
55
|
+
.plan{background:hsl(var(--card));border:1px solid hsl(var(--border));border-radius:12px;padding:4px 16px}
|
|
56
|
+
.plan>.kicker{margin-top:12px}
|
|
57
|
+
.kicker{font-size:12px;font-weight:600;color:hsl(var(--muted-foreground));text-transform:uppercase;letter-spacing:.04em}
|
|
58
|
+
.error{color:hsl(var(--destructive));border:1px solid hsl(var(--destructive)/.4);border-radius:10px;padding:8px 14px;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
59
|
+
.divider{display:flex;align-items:center;gap:12px;color:hsl(var(--muted-foreground));font-size:12px}
|
|
60
|
+
.divider::before,.divider::after{content:"";flex:1;border-top:1px solid hsl(var(--border))}
|
|
61
|
+
details.tool{border:1px solid hsl(var(--border));border-radius:10px;background:hsl(var(--card))}
|
|
62
|
+
details.tool summary{cursor:pointer;padding:8px 12px;font-size:13px;color:hsl(var(--muted-foreground));overflow-wrap:anywhere}
|
|
63
|
+
details.tool pre{margin:0;padding:10px 12px;border-top:1px solid hsl(var(--border));max-height:420px;overflow:auto}
|
|
64
|
+
.assistant>:first-child{margin-top:0}.assistant>:last-child{margin-bottom:0}
|
|
65
|
+
p,ul,ol,blockquote,table,pre{margin:.7em 0}
|
|
66
|
+
a{color:hsl(var(--primary))}
|
|
67
|
+
blockquote{margin-left:0;padding-left:12px;border-left:3px solid hsl(var(--border));color:hsl(var(--muted-foreground))}
|
|
68
|
+
code{font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
69
|
+
:not(pre)>code{background:hsl(var(--muted));border-radius:5px;padding:1px 5px}
|
|
70
|
+
pre{background:hsl(var(--card));border:1px solid hsl(var(--border));border-radius:10px;padding:12px 14px;overflow:auto;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
71
|
+
pre code{background:none;padding:0}
|
|
72
|
+
table{border-collapse:collapse;display:block;overflow:auto}
|
|
73
|
+
th,td{border:1px solid hsl(var(--border));padding:6px 10px;text-align:left}
|
|
74
|
+
th{background:hsl(var(--muted))}
|
|
75
|
+
img{max-width:100%}
|
|
76
|
+
.artifact{border:1px solid hsl(var(--border));border-radius:12px;overflow:hidden;background:hsl(var(--card))}
|
|
77
|
+
.artifact .kicker{padding:8px 12px;border-bottom:1px solid hsl(var(--border))}
|
|
78
|
+
.artifact iframe{display:block;width:100%;border:0}
|
|
79
|
+
footer{margin-top:40px;padding-top:16px;border-top:1px solid hsl(var(--border));color:hsl(var(--muted-foreground));font-size:12px}
|
|
80
|
+
@media (max-width:520px){.wrap{padding:16px 12px 48px}.user .bubble{max-width:95%}}
|
|
81
|
+
`
|
|
82
|
+
.trim()
|
|
83
|
+
.replace(/\n+/g, "\n");
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { proxy } from "valtio";
|
|
2
|
+
import type { ShareTarget } from "@/data/shares";
|
|
3
|
+
|
|
4
|
+
// What the share dialog is open on: a chat or an artifact, started from a
|
|
5
|
+
// menu, the pane or the artifact card.
|
|
6
|
+
export const shareUi = proxy<{
|
|
7
|
+
target: (ShareTarget & { title: string }) | null;
|
|
8
|
+
}>({ target: null });
|
|
9
|
+
|
|
10
|
+
export const shareUiActions = {
|
|
11
|
+
open: (target: ShareTarget & { title: string }) => {
|
|
12
|
+
shareUi.target = target;
|
|
13
|
+
},
|
|
14
|
+
close: () => {
|
|
15
|
+
shareUi.target = null;
|
|
16
|
+
},
|
|
17
|
+
};
|