@saadnvd1/agent-os 0.3.761 → 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/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/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/package.json +1 -1
- package/stores/shareUi.ts +6 -2
package/lib/artifacts/tools.ts
CHANGED
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
type McpSdkServerConfigWithInstance,
|
|
14
14
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
saveArtifact,
|
|
17
17
|
MAX_HTML_BYTES,
|
|
18
|
+
MAX_NOTE_CHARS,
|
|
18
19
|
MAX_TITLE_CHARS,
|
|
19
20
|
type Artifact,
|
|
20
21
|
} from "./store";
|
|
@@ -84,20 +85,31 @@ export interface VisualsContext {
|
|
|
84
85
|
// Saves the page and shows it in the conversation.
|
|
85
86
|
export async function renderPage(
|
|
86
87
|
ctx: VisualsContext,
|
|
87
|
-
args: {
|
|
88
|
+
args: {
|
|
89
|
+
html?: string;
|
|
90
|
+
path?: string;
|
|
91
|
+
title: string;
|
|
92
|
+
height?: number;
|
|
93
|
+
update?: string;
|
|
94
|
+
note?: string;
|
|
95
|
+
}
|
|
88
96
|
): Promise<CallToolResult> {
|
|
89
97
|
try {
|
|
90
|
-
const artifact =
|
|
98
|
+
const { artifact, created } = saveArtifact(
|
|
91
99
|
ctx.sessionId,
|
|
92
100
|
args.title,
|
|
93
|
-
readSource(args, ctx.cwd)
|
|
101
|
+
readSource(args, ctx.cwd),
|
|
102
|
+
{ update: args.update, note: args.note, sameTitle: true }
|
|
94
103
|
);
|
|
95
|
-
ctx.onRender(artifact, args.height);
|
|
104
|
+
if (created) ctx.onRender(artifact, args.height);
|
|
105
|
+
const name = `"${artifact.title}"${artifact.version > 1 ? ` v${artifact.version}` : ""}`;
|
|
96
106
|
return {
|
|
97
107
|
content: [
|
|
98
108
|
{
|
|
99
109
|
type: "text",
|
|
100
|
-
text:
|
|
110
|
+
text: created
|
|
111
|
+
? `Shown in the chat as ${name} (artifact ${artifact.id}). To change it, call html_render again with update: "${artifact.id}"; every version is kept.`
|
|
112
|
+
: `Unchanged: the page is the same as ${name} (artifact ${artifact.id}), already in the chat.`,
|
|
101
113
|
},
|
|
102
114
|
],
|
|
103
115
|
};
|
|
@@ -171,6 +183,17 @@ export function visualsTools(
|
|
|
171
183
|
.describe(
|
|
172
184
|
"Cap on the frame's height in CSS pixels; longer pages scroll inside it. Defaults to fitting the page."
|
|
173
185
|
),
|
|
186
|
+
update: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe(
|
|
190
|
+
"The artifact id of a page you already showed in this chat, to show a new version of it. A page with the same title is updated too. Every version is kept."
|
|
191
|
+
),
|
|
192
|
+
note: z
|
|
193
|
+
.string()
|
|
194
|
+
.max(MAX_NOTE_CHARS)
|
|
195
|
+
.optional()
|
|
196
|
+
.describe("For a new version: one line on what changed."),
|
|
174
197
|
},
|
|
175
198
|
(args) => renderPage(ctx, args)
|
|
176
199
|
),
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A page's versions for the version switcher: each with its note, size and
|
|
2
|
+
// the message that asked for it.
|
|
3
|
+
|
|
4
|
+
import { db } from "../db";
|
|
5
|
+
import { getArtifact, listVersions, type Artifact } from "./store";
|
|
6
|
+
import {
|
|
7
|
+
pageActions,
|
|
8
|
+
type ArtifactVersion,
|
|
9
|
+
type PageVersions,
|
|
10
|
+
} from "./actions";
|
|
11
|
+
|
|
12
|
+
const PROMPT_CHARS = 80;
|
|
13
|
+
|
|
14
|
+
const sqliteMs = (t: string) =>
|
|
15
|
+
Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`) || 0;
|
|
16
|
+
|
|
17
|
+
// The user message before the chat item that showed this version.
|
|
18
|
+
function turnOf(a: Artifact): ArtifactVersion["turn"] {
|
|
19
|
+
const row = db
|
|
20
|
+
.prepare(
|
|
21
|
+
`SELECT u.item_id AS itemId,
|
|
22
|
+
json_extract(u.data, '$.text') AS text,
|
|
23
|
+
(SELECT COUNT(*) FROM chat_items c
|
|
24
|
+
WHERE c.session_id = u.session_id AND c.kind = 'user'
|
|
25
|
+
AND c.seq <= u.seq) AS idx
|
|
26
|
+
FROM chat_items u
|
|
27
|
+
WHERE u.session_id = ? AND u.kind = 'user'
|
|
28
|
+
AND u.seq < (SELECT seq FROM chat_items
|
|
29
|
+
WHERE session_id = ? AND item_id = ?)
|
|
30
|
+
ORDER BY u.seq DESC LIMIT 1`
|
|
31
|
+
)
|
|
32
|
+
.get(a.session_id, a.session_id, `artifact-${a.id}`) as
|
|
33
|
+
{ itemId: string; text: string | null; idx: number } | undefined;
|
|
34
|
+
if (!row) return undefined;
|
|
35
|
+
const prompt = (row.text ?? "").replace(/\s+/g, " ").trim();
|
|
36
|
+
return {
|
|
37
|
+
itemId: row.itemId,
|
|
38
|
+
index: row.idx,
|
|
39
|
+
prompt:
|
|
40
|
+
prompt.length > PROMPT_CHARS
|
|
41
|
+
? `${prompt.slice(0, PROMPT_CHARS - 1)}…`
|
|
42
|
+
: prompt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function toVersion(a: Artifact): ArtifactVersion {
|
|
47
|
+
const turn = turnOf(a);
|
|
48
|
+
return {
|
|
49
|
+
id: a.id,
|
|
50
|
+
version: a.version,
|
|
51
|
+
title: a.title,
|
|
52
|
+
...(a.note && { note: a.note }),
|
|
53
|
+
createdAt: sqliteMs(a.created_at),
|
|
54
|
+
...(a.bytes != null && { bytes: a.bytes }),
|
|
55
|
+
...(turn && { turn }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Every version of the page that artifactId is a version of.
|
|
60
|
+
export function pageVersions(
|
|
61
|
+
artifactId: string,
|
|
62
|
+
ctx: { pdf: boolean }
|
|
63
|
+
): PageVersions | undefined {
|
|
64
|
+
const a = getArtifact(artifactId);
|
|
65
|
+
if (!a) return undefined;
|
|
66
|
+
const versions = listVersions(a.page_id).map(toVersion);
|
|
67
|
+
return {
|
|
68
|
+
pageId: a.page_id,
|
|
69
|
+
latestId: versions[versions.length - 1]?.id ?? a.id,
|
|
70
|
+
versions,
|
|
71
|
+
actions: pageActions(ctx),
|
|
72
|
+
};
|
|
73
|
+
}
|
package/lib/chat/events.ts
CHANGED
|
@@ -254,6 +254,10 @@ export type ChatItem =
|
|
|
254
254
|
kind: "artifact";
|
|
255
255
|
artifactId: string;
|
|
256
256
|
title: string;
|
|
257
|
+
// The page it's a version of, and which; absent before versions (v1 of
|
|
258
|
+
// its own page).
|
|
259
|
+
pageId?: string;
|
|
260
|
+
version?: number;
|
|
257
261
|
// A cap on the frame's height; it fits the page otherwise.
|
|
258
262
|
height?: number;
|
|
259
263
|
})
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "20261029T1200_artifact_versions",
|
|
5
|
+
up: (db) => {
|
|
6
|
+
// Each artifacts row is one version of a page; rows sharing page_id are
|
|
7
|
+
// that page's history. Versions are only ever added, never rewritten.
|
|
8
|
+
const cols = new Set(
|
|
9
|
+
(
|
|
10
|
+
db.prepare(`PRAGMA table_info(artifacts)`).all() as { name: string }[]
|
|
11
|
+
).map((c) => c.name)
|
|
12
|
+
);
|
|
13
|
+
const add = (name: string, type: string) => {
|
|
14
|
+
if (!cols.has(name))
|
|
15
|
+
db.exec(`ALTER TABLE artifacts ADD COLUMN ${name} ${type}`);
|
|
16
|
+
};
|
|
17
|
+
add("page_id", "TEXT");
|
|
18
|
+
add("version", "INTEGER NOT NULL DEFAULT 1");
|
|
19
|
+
// The agent's one line on what changed.
|
|
20
|
+
add("note", "TEXT");
|
|
21
|
+
add("bytes", "INTEGER");
|
|
22
|
+
add("sha256", "TEXT");
|
|
23
|
+
db.exec(`UPDATE artifacts SET page_id = id WHERE page_id IS NULL`);
|
|
24
|
+
db.exec(
|
|
25
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_page_version ON artifacts(page_id, version)`
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
} satisfies Migration;
|
package/lib/security/demo.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { db, queries, type Session } from "../db";
|
|
|
12
12
|
import { flushItems } from "../chat/store";
|
|
13
13
|
import { git } from "../git-exec";
|
|
14
14
|
import { PLAN_FILE } from "../models/plan-step";
|
|
15
|
-
import { listArtifacts } from "../artifacts/store";
|
|
15
|
+
import { listArtifacts, type Artifact } from "../artifacts/store";
|
|
16
16
|
import { allowedRoots, within } from "./access";
|
|
17
17
|
import { SCREENSHOT_DIR } from "../tasks/ui-brief";
|
|
18
18
|
import {
|
|
@@ -51,6 +51,7 @@ const ITEMS_SQL = `
|
|
|
51
51
|
json_extract(data, '$.input.filePath') AS file_path_camel,
|
|
52
52
|
json_extract(data, '$.input.changes') AS changes,
|
|
53
53
|
json_extract(data, '$.artifactId') AS artifact_id,
|
|
54
|
+
json_extract(data, '$.pageId') AS page_id,
|
|
54
55
|
json_extract(data, '$.title') AS title,
|
|
55
56
|
json_extract(data, '$.taskId') AS task_id,
|
|
56
57
|
json_extract(data, '$.outputFile') AS output_file,
|
|
@@ -202,16 +203,37 @@ async function build(session: Session): Promise<Entry[]> {
|
|
|
202
203
|
const keptBase = sessionKeptDir(session.id);
|
|
203
204
|
const tracked = new Set(manifest?.tracked);
|
|
204
205
|
const kept = new Set(manifest?.kept);
|
|
205
|
-
const
|
|
206
|
+
const saved = listArtifacts(session.id);
|
|
207
|
+
const byId = new Map(saved.map((a) => [a.id, a]));
|
|
208
|
+
// Each page's newest version and how many it has.
|
|
209
|
+
const newest = new Map<string, Artifact>();
|
|
210
|
+
const count = new Map<string, number>();
|
|
211
|
+
for (const a of saved) {
|
|
212
|
+
count.set(a.page_id, (count.get(a.page_id) ?? 0) + 1);
|
|
213
|
+
if ((newest.get(a.page_id)?.version ?? 0) < a.version)
|
|
214
|
+
newest.set(a.page_id, a);
|
|
215
|
+
}
|
|
206
216
|
const entries = await Promise.all(
|
|
207
217
|
candidates.map(async (c): Promise<Entry> => {
|
|
208
218
|
if (c.source.type === "page") {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
const seen = byId.get(c.source.artifactId);
|
|
220
|
+
const page = seen && (newest.get(seen.page_id) ?? seen);
|
|
221
|
+
const size = page ? await sizeOf(page.path) : undefined;
|
|
222
|
+
const c2 = page
|
|
223
|
+
? { ...c, source: { type: "page" as const, artifactId: page.id } }
|
|
224
|
+
: c;
|
|
225
|
+
const entry = toArtifact(
|
|
226
|
+
c2,
|
|
213
227
|
size === undefined ? { state: "gone" } : { state: "live", size }
|
|
214
228
|
);
|
|
229
|
+
if (page)
|
|
230
|
+
Object.assign(entry.artifact, {
|
|
231
|
+
name: page.title,
|
|
232
|
+
artifactId: page.id,
|
|
233
|
+
version: page.version,
|
|
234
|
+
versionCount: count.get(page.page_id) ?? 1,
|
|
235
|
+
});
|
|
236
|
+
return entry;
|
|
215
237
|
}
|
|
216
238
|
const file = filePathOf(c);
|
|
217
239
|
if (!file) return toArtifact(c, null);
|
|
@@ -29,6 +29,7 @@ export interface ItemRow {
|
|
|
29
29
|
file_path_camel?: string | null;
|
|
30
30
|
changes?: string | null;
|
|
31
31
|
artifact_id?: string | null;
|
|
32
|
+
page_id?: string | null;
|
|
32
33
|
title?: string | null;
|
|
33
34
|
task_id?: string | null;
|
|
34
35
|
output_file?: string | null;
|
|
@@ -177,7 +178,8 @@ export function fromItems(
|
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
} else if (row.kind === "artifact" && row.artifact_id) {
|
|
180
|
-
|
|
181
|
+
// One row per page: a later version replaces an earlier one.
|
|
182
|
+
const key = `page:${row.page_id || row.artifact_id}`;
|
|
181
183
|
out.set(key, {
|
|
182
184
|
key,
|
|
183
185
|
kind: "page",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// What a session produced, as the Artifacts view lists it. Client-safe.
|
|
2
2
|
|
|
3
|
+
import type { ArtifactAction } from "../artifacts/actions";
|
|
4
|
+
|
|
3
5
|
export type ArtifactKind =
|
|
4
6
|
"plan" | "file" | "page" | "image" | "log" | "pr" | "run" | "session-log";
|
|
5
7
|
|
|
@@ -30,6 +32,13 @@ export interface SessionArtifact {
|
|
|
30
32
|
folder?: string;
|
|
31
33
|
// A link out (the PR on GitHub).
|
|
32
34
|
url?: string;
|
|
35
|
+
// A page: its latest version's artifact id, which version that is, and
|
|
36
|
+
// how many there are.
|
|
37
|
+
artifactId?: string;
|
|
38
|
+
version?: number;
|
|
39
|
+
versionCount?: number;
|
|
40
|
+
// What the viewer can do with it; set by the server.
|
|
41
|
+
actions?: ArtifactAction[];
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
export const openable = (a: Pick<SessionArtifact, "state" | "preview">) =>
|
package/package.json
CHANGED
package/stores/shareUi.ts
CHANGED
|
@@ -3,12 +3,16 @@ import type { ShareTarget } from "@/data/shares";
|
|
|
3
3
|
|
|
4
4
|
// What the share dialog is open on: a chat or an artifact, started from a
|
|
5
5
|
// menu, the pane or the artifact card.
|
|
6
|
+
// A page's version, when it has more than one, is named in the title; the
|
|
7
|
+
// share is of that exact version.
|
|
8
|
+
export type ShareUiTarget = ShareTarget & { title: string; version?: number };
|
|
9
|
+
|
|
6
10
|
export const shareUi = proxy<{
|
|
7
|
-
target:
|
|
11
|
+
target: ShareUiTarget | null;
|
|
8
12
|
}>({ target: null });
|
|
9
13
|
|
|
10
14
|
export const shareUiActions = {
|
|
11
|
-
open: (target:
|
|
15
|
+
open: (target: ShareUiTarget) => {
|
|
12
16
|
shareUi.target = target;
|
|
13
17
|
},
|
|
14
18
|
close: () => {
|