@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
package/lib/artifacts/store.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { agentosHome } from "../agentos-home";
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import { randomUUID } from "crypto";
|
|
9
|
+
import { createHash, randomUUID } from "crypto";
|
|
10
10
|
import { db } from "../db";
|
|
11
11
|
|
|
12
12
|
export interface Artifact {
|
|
@@ -15,8 +15,31 @@ export interface Artifact {
|
|
|
15
15
|
title: string;
|
|
16
16
|
path: string;
|
|
17
17
|
created_at: string;
|
|
18
|
+
// The page this is a version of; every version of a page shares it.
|
|
19
|
+
page_id: string;
|
|
20
|
+
version: number;
|
|
21
|
+
// The agent's one line on what changed.
|
|
22
|
+
note: string | null;
|
|
23
|
+
bytes: number | null;
|
|
24
|
+
sha256: string | null;
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
export interface SaveOptions {
|
|
28
|
+
// Any version's id of a page in this session: add a version to it.
|
|
29
|
+
update?: string;
|
|
30
|
+
// With no update, the newest page in this session with the same title.
|
|
31
|
+
sameTitle?: boolean;
|
|
32
|
+
note?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Saved {
|
|
36
|
+
artifact: Artifact;
|
|
37
|
+
// False when the page was byte-for-byte its latest version already.
|
|
38
|
+
created: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const MAX_NOTE_CHARS = 120;
|
|
42
|
+
|
|
20
43
|
export const MAX_HTML_BYTES = 2 * 1024 * 1024;
|
|
21
44
|
export const MAX_TITLE_CHARS = 120;
|
|
22
45
|
|
|
@@ -35,20 +58,92 @@ function sessionDir(sessionId: string): string {
|
|
|
35
58
|
export function createArtifact(
|
|
36
59
|
sessionId: string,
|
|
37
60
|
title: string,
|
|
38
|
-
html: string
|
|
61
|
+
html: string,
|
|
62
|
+
opts: SaveOptions = {}
|
|
39
63
|
): Artifact {
|
|
40
|
-
|
|
64
|
+
return saveArtifact(sessionId, title, html, opts).artifact;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const oneLine = (s: string, max: number) =>
|
|
68
|
+
s.replace(/\s+/g, " ").trim().slice(0, max);
|
|
69
|
+
|
|
70
|
+
// Saves a page, or a new version of one. Earlier versions are never touched.
|
|
71
|
+
export function saveArtifact(
|
|
72
|
+
sessionId: string,
|
|
73
|
+
title: string,
|
|
74
|
+
html: string,
|
|
75
|
+
opts: SaveOptions = {}
|
|
76
|
+
): Saved {
|
|
77
|
+
const bytes = Buffer.byteLength(html, "utf8");
|
|
78
|
+
if (bytes > MAX_HTML_BYTES)
|
|
41
79
|
throw new Error("The page is over 2 MB; make it smaller.");
|
|
80
|
+
const clean = oneLine(title, MAX_TITLE_CHARS) || "Untitled";
|
|
81
|
+
const note = opts.note ? oneLine(opts.note, MAX_NOTE_CHARS) || null : null;
|
|
82
|
+
const sha256 = createHash("sha256").update(html).digest("hex");
|
|
42
83
|
const id = randomUUID();
|
|
43
84
|
const dir = sessionDir(sessionId);
|
|
44
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
45
85
|
const file = path.join(dir, `${id}.html`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
86
|
+
return db.transaction((): Saved => {
|
|
87
|
+
const pageId = resolvePage(sessionId, clean, opts);
|
|
88
|
+
const latest = pageId ? latestVersion(pageId) : undefined;
|
|
89
|
+
if (latest && latest.sha256 === sha256)
|
|
90
|
+
return { artifact: latest, created: false };
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
92
|
+
fs.writeFileSync(file, html, { mode: 0o600 });
|
|
93
|
+
db.prepare(
|
|
94
|
+
`INSERT INTO artifacts (id, session_id, title, path, page_id, version, note, bytes, sha256)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
96
|
+
).run(
|
|
97
|
+
id,
|
|
98
|
+
sessionId,
|
|
99
|
+
clean,
|
|
100
|
+
file,
|
|
101
|
+
pageId ?? id,
|
|
102
|
+
latest ? latest.version + 1 : 1,
|
|
103
|
+
note,
|
|
104
|
+
bytes,
|
|
105
|
+
sha256
|
|
106
|
+
);
|
|
107
|
+
return { artifact: getArtifact(id)!, created: true };
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolvePage(
|
|
112
|
+
sessionId: string,
|
|
113
|
+
title: string,
|
|
114
|
+
opts: SaveOptions
|
|
115
|
+
): string | undefined {
|
|
116
|
+
if (opts.update) {
|
|
117
|
+
const base = getArtifact(opts.update);
|
|
118
|
+
if (!base || base.session_id !== sessionId)
|
|
119
|
+
throw new Error(
|
|
120
|
+
`No page ${opts.update} in this chat; leave out update to show a new page.`
|
|
121
|
+
);
|
|
122
|
+
return base.page_id;
|
|
123
|
+
}
|
|
124
|
+
if (!opts.sameTitle) return undefined;
|
|
125
|
+
const row = db
|
|
126
|
+
.prepare(
|
|
127
|
+
`SELECT page_id FROM artifacts WHERE session_id = ? AND lower(title) = lower(?)
|
|
128
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`
|
|
129
|
+
)
|
|
130
|
+
.get(sessionId, title) as { page_id: string } | undefined;
|
|
131
|
+
return row?.page_id;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Every version of a page, oldest first.
|
|
135
|
+
export function listVersions(pageId: string): Artifact[] {
|
|
136
|
+
return db
|
|
137
|
+
.prepare(`SELECT * FROM artifacts WHERE page_id = ? ORDER BY version`)
|
|
138
|
+
.all(pageId) as Artifact[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function latestVersion(pageId: string): Artifact | undefined {
|
|
142
|
+
return db
|
|
143
|
+
.prepare(
|
|
144
|
+
`SELECT * FROM artifacts WHERE page_id = ? ORDER BY version DESC LIMIT 1`
|
|
145
|
+
)
|
|
146
|
+
.get(pageId) as Artifact | undefined;
|
|
52
147
|
}
|
|
53
148
|
|
|
54
149
|
export function getArtifact(id: string): Artifact | undefined {
|
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/commands.ts
CHANGED
|
@@ -11,6 +11,15 @@ export function slashQuery(
|
|
|
11
11
|
return { query: m[2].toLowerCase(), start: m.index + m[1].length };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// The SDK ends a description with where the command comes from, "(user)",
|
|
15
|
+
// "(project)" or "(plugin-name)"; the menu's group header says that already.
|
|
16
|
+
export function stripSourceSuffix(description: string): string {
|
|
17
|
+
return description.replace(
|
|
18
|
+
/\s*\((?:user|project|builtin|plugin|[\w.-]+@[\w.-]+)\)\s*$/i,
|
|
19
|
+
""
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
function isSubsequence(q: string, s: string): boolean {
|
|
15
24
|
let i = 0;
|
|
16
25
|
for (const ch of s) if (ch === q[i]) i++;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import type { ChatCommand, ChatItem, DriverEvent } from "../events";
|
|
3
3
|
import { clipOutput, toolDiff, toolTitle } from "../tools";
|
|
4
|
-
import { leadingCommand } from "../commands";
|
|
4
|
+
import { leadingCommand, stripSourceSuffix } from "../commands";
|
|
5
5
|
import { usageTotals } from "../context";
|
|
6
6
|
|
|
7
7
|
// Loose views of the SDK's message shapes: only what the mapper reads.
|
|
@@ -93,7 +93,7 @@ export type SdkCommand = {
|
|
|
93
93
|
export function toCommand(c: SdkCommand): ChatCommand {
|
|
94
94
|
return {
|
|
95
95
|
name: c.name,
|
|
96
|
-
description: c.description ?? "",
|
|
96
|
+
description: stripSourceSuffix(c.description ?? ""),
|
|
97
97
|
argumentHint: c.argumentHint || undefined,
|
|
98
98
|
builtin: c.builtin,
|
|
99
99
|
};
|
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/next.config.ts
CHANGED
|
@@ -7,6 +7,12 @@ const nextConfig: NextConfig = {
|
|
|
7
7
|
// so the live server never serves a half-written build.
|
|
8
8
|
distDir: process.env.AGENTOS_DIST_DIR || ".next",
|
|
9
9
|
devIndicators: false,
|
|
10
|
+
// Next 16.4 stops `next dev` once under an agent to print an upgrade
|
|
11
|
+
// nudge, which kills every task's dev server. Upgrades go through
|
|
12
|
+
// Dependabot and tasks instead.
|
|
13
|
+
experimental: {
|
|
14
|
+
agentUpgrade: false,
|
|
15
|
+
},
|
|
10
16
|
turbopack: {
|
|
11
17
|
root: resolve(import.meta.dirname),
|
|
12
18
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saadnvd1/agent-os",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.762",
|
|
4
4
|
"description": "Self-hosted web UI for managing Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-os": "scripts/agent-os"
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"mermaid": "^12.2.0",
|
|
137
137
|
"micromark-extension-gfm": "^3.0.0",
|
|
138
138
|
"monaco-editor": "^0.57.0",
|
|
139
|
-
"next": "^16.
|
|
139
|
+
"next": "^16.4.2",
|
|
140
140
|
"next-themes": "^0.4.6",
|
|
141
141
|
"node-pty": "^1.2.0-beta.6",
|
|
142
142
|
"playwright": "^1.63.0",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"@types/react-dom": "^19.2.3",
|
|
168
168
|
"@types/ws": "^8.18.1",
|
|
169
169
|
"eslint": "^9.39.5",
|
|
170
|
-
"eslint-config-next": "^16.
|
|
170
|
+
"eslint-config-next": "^16.4.2",
|
|
171
171
|
"husky": "^9.1.7",
|
|
172
172
|
"jsdom": "^30.1.2",
|
|
173
173
|
"prettier": "^3.7.4",
|
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: () => {
|