@saadnvd1/agent-os 0.3.595 → 0.3.597
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/sessions/[id]/artifacts/[artifactId]/route.ts +34 -0
- package/app/api/sessions/[id]/artifacts/route.ts +25 -9
- package/components/Artifacts/ArtifactPreview.tsx +239 -0
- package/components/Artifacts/ArtifactRow.tsx +147 -0
- package/components/Artifacts/ArtifactsPanel.tsx +244 -0
- package/components/Artifacts/helpers.test.ts +90 -0
- package/components/Artifacts/helpers.ts +99 -0
- package/components/Artifacts/index.ts +1 -0
- package/components/Chat/ArtifactFrame.tsx +4 -1
- package/components/Pane/DesktopTabBar.tsx +2 -2
- package/components/Pane/MobileTabBar.tsx +14 -2
- package/components/Pane/PaneViewToggle.tsx +20 -3
- package/components/Pane/index.tsx +35 -3
- package/contexts/PaneContext.tsx +7 -1
- package/data/session-artifacts/index.ts +2 -0
- package/data/session-artifacts/keys.ts +7 -0
- package/data/session-artifacts/queries.ts +53 -0
- package/lib/chat/task-output.ts +1 -1
- package/lib/db/migration-types.ts +15 -0
- package/lib/db/migrations/0001_add_group_path_to_sessions.ts +11 -0
- package/lib/db/migrations/0002_add_agent_type_to_sessions.ts +11 -0
- package/lib/db/migrations/0003_add_worktree_columns_to_sessions.ts +12 -0
- package/lib/db/migrations/0004_add_pr_tracking_to_sessions.ts +11 -0
- package/lib/db/migrations/0005_add_group_path_index.ts +11 -0
- package/lib/db/migrations/0006_add_orchestration_columns_to_sessions.ts +16 -0
- package/lib/db/migrations/0007_add_auto_approve_to_sessions.ts +11 -0
- package/lib/db/migrations/0008_add_dev_server_columns.ts +19 -0
- package/lib/db/migrations/0009_add_project_id_to_sessions.ts +17 -0
- package/lib/db/migrations/0010_add_project_id_to_dev_servers.ts +36 -0
- package/lib/db/migrations/0011_add_tmux_name_to_sessions.ts +13 -0
- package/lib/db/migrations/0012_add_initial_prompt_to_projects.ts +9 -0
- package/lib/db/migrations/0013_add_project_repositories_table.ts +22 -0
- package/lib/db/migrations/0014_add_hosts.ts +23 -0
- package/lib/db/migrations/0015_add_workspaces.ts +18 -0
- package/lib/db/migrations/0016_add_task_columns_to_sessions.ts +13 -0
- package/lib/db/migrations/0017_add_bus_messages.ts +24 -0
- package/lib/db/migrations/0018_add_chat.ts +23 -0
- package/lib/db/migrations/0019_add_lumifyhub.ts +36 -0
- package/lib/db/migrations/0020_add_lumifyhub_published_docs.ts +18 -0
- package/lib/db/migrations/0021_add_chat_access.ts +14 -0
- package/lib/db/migrations/0022_add_last_seen.ts +10 -0
- package/lib/db/migrations/0023_mark_existing_sessions_seen.ts +12 -0
- package/lib/db/migrations/0024_add_stacks.ts +49 -0
- package/lib/db/migrations/0025_add_stack_item_attempts.ts +16 -0
- package/lib/db/migrations/0026_add_workspace_orchestrator.ts +47 -0
- package/lib/db/migrations/0027_add_devices_and_settings.ts +28 -0
- package/lib/db/migrations/0028_add_orchestrator_acting.ts +70 -0
- package/lib/db/migrations/0029_add_orchestrator_asks_and_pause.ts +81 -0
- package/lib/db/migrations/0030_add_sessions_archived_at.ts +14 -0
- package/lib/db/migrations/0031_add_stack_item_restacked_heads.ts +22 -0
- package/lib/db/migrations/0032_add_sessions_pinned.ts +12 -0
- package/lib/db/migrations/0033_add_program_status.ts +18 -0
- package/lib/db/migrations/0034_add_session_names.ts +21 -0
- package/lib/db/migrations/0035_add_artifacts.ts +23 -0
- package/lib/db/migrations/0036_add_chat_queue.ts +26 -0
- package/lib/db/migrations/0037_add_chat_plan_and_turns.ts +39 -0
- package/lib/db/migrations/0038_add_schedules.ts +56 -0
- package/lib/db/migrations/0039_add_session_name_source.ts +21 -0
- package/lib/db/migrations/0040_add_schedule_targets_and_phone_notify.ts +41 -0
- package/lib/db/migrations/0041_add_task_setup.ts +17 -0
- package/lib/db/migrations/0042_add_host_links_and_moved_tasks.ts +29 -0
- package/lib/db/migrations/0043_scratch_project.ts +14 -0
- package/lib/db/migrations/0044_chat_item_kind.ts +21 -0
- package/lib/db/migrations/0045_change_versions.ts +13 -0
- package/lib/db/migrations/0046_session_port_slots.ts +21 -0
- package/lib/db/migrations/0047_pin_orchestrators.ts +12 -0
- package/lib/db/migrations/0048_session_database.ts +15 -0
- package/lib/db/migrations/0049_peer_mirror.ts +14 -0
- package/lib/db/migrations/0050_task_queue.ts +44 -0
- package/lib/db/migrations/0051_chat_queue_sent_by.ts +16 -0
- package/lib/db/migrations/0052_sessions_chat_restarted.ts +17 -0
- package/lib/db/migrations/0053_brief_amendments_failure_sha_ask_raised_by.ts +37 -0
- package/lib/db/migrations/0054_sessions_chat_tools_digest.ts +15 -0
- package/lib/db/migrations/0055_sessions_merge_armed_and_rebase.ts +25 -0
- package/lib/db/migrations/0056_orchestrator_rules.ts +39 -0
- package/lib/db/migrations/0057_task_pr_wakes.ts +34 -0
- package/lib/db/migrations/0058_away_mode.ts +71 -0
- package/lib/db/migrations/0059_gate_failure_commits.ts +33 -0
- package/lib/db/migrations/0060_check_runs_and_compactions.ts +44 -0
- package/lib/db/migrations/0061_model_routing.ts +30 -0
- package/lib/db/migrations/0062_sessions_quick_of.ts +19 -0
- package/lib/db/migrations/0063_one_off_schedules.ts +16 -0
- package/lib/db/migrations/0064_chat_compactions_phase.ts +15 -0
- package/lib/db/migrations/0065_orchestrator_triage.ts +23 -0
- package/lib/db/migrations/0066_workspace_orch_mcp_servers.ts +16 -0
- package/lib/db/migrations/0067_orchestrator_event_log_suppressed.ts +14 -0
- package/lib/db/migrations/0068_usage_calls_and_rollups.ts +74 -0
- package/lib/db/migrations/0069_session_logs.ts +26 -0
- package/lib/db/migrations/0070_chat_items_running.ts +15 -0
- package/lib/db/migrations/0071_devices_push.ts +29 -0
- package/lib/db/migrations/0072_push_outbox.ts +33 -0
- package/lib/db/migrations/0073_orchestrated_task_moves.ts +27 -0
- package/lib/db/migrations/0074_held_task_replies.ts +27 -0
- package/lib/db/migrations/0075_plan_only_tasks.ts +17 -0
- package/lib/db/migrations/0076_welcome_backfill.ts +16 -0
- package/lib/db/migrations/0077_simulator_leases.ts +21 -0
- package/lib/db/migrations/0078_orchestrator_questions.ts +34 -0
- package/lib/db/migrations/0079_task_queue_agent.ts +13 -0
- package/lib/db/migrations/0080_recipes.ts +129 -0
- package/lib/db/migrations/0081_schedules_into_recipes.ts +18 -0
- package/lib/db/migrations/0082_session_shelf_times.ts +23 -0
- package/lib/db/migrations/0083_native_devices_can_approve.ts +18 -0
- package/lib/db/migrations/0084_approval_audit.ts +20 -0
- package/lib/db/migrations/0085_experiment_usage.ts +35 -0
- package/lib/db/migrations/0086_learning_cards.ts +28 -0
- package/lib/db/migrations/0087_plan_pieces_reconcile.ts +29 -0
- package/lib/db/migrations/0088_shared_links.ts +25 -0
- package/lib/db/migrations/0089_passkeys_device_keys.ts +23 -0
- package/lib/db/migrations/20261014T0900_task_scheduling.ts +57 -0
- package/lib/db/migrations/20261014T1235_placed_mirror_started_by.ts +19 -0
- package/lib/db/migrations/20261014T1600_queued_task_messages.ts +21 -0
- package/lib/db/migrations/20261014T1800_task_starter_device.ts +20 -0
- package/lib/db/migrations/20261014T1900_pause_note.ts +13 -0
- package/lib/db/migrations/20261014T2000_chat_bookmarks.ts +33 -0
- package/lib/db/migrations/20261014T2100_gate_failure_ask_held.ts +18 -0
- package/lib/db/migrations/README.md +35 -0
- package/lib/db/migrations-legacy-checksums.json +9 -8
- package/lib/db/migrations-split-snapshot.json +1759 -0
- package/lib/db/migrations.ts +76 -2337
- package/lib/done/worktree.ts +3 -0
- package/lib/security/demo.ts +1 -0
- package/lib/session-artifacts/access.ts +81 -0
- package/lib/session-artifacts/collect.ts +318 -0
- package/lib/session-artifacts/derive.ts +437 -0
- package/lib/session-artifacts/keep.ts +119 -0
- package/lib/session-artifacts/peer.ts +187 -0
- package/lib/session-artifacts/serve.ts +265 -0
- package/lib/session-artifacts/types.ts +43 -0
- package/lib/tasks/finish.ts +2 -0
- package/package.json +6 -3
- package/scripts/check +1 -0
- package/scripts/gen-migrations-index.mjs +44 -0
- package/scripts/screenshots/demo.ts +5 -1
- package/scripts/screenshots/run.ts +5 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ and check on all of it from your phone.
|
|
|
21
21
|
- **Agents that talk to each other.** Sessions message each other with `aos`, and you see every conversation in Messages. [More](docs/guides/agent-network.md)
|
|
22
22
|
- **Chat or terminal, same conversation.** Streaming replies, steps and diffs, plan mode, slash commands, `@file`, undo. [More](docs/guides/chat.md)
|
|
23
23
|
- **Bookmarks.** Star any message, tool call or turn, then find it again from the chat or across every chat, with a search. [More](docs/guides/chat.md)
|
|
24
|
+
- **Every artifact in one place.** A session's plan, PR, files, pages, images and job logs in one list, with a preview, kept after its worktree is removed. [More](docs/guides/sessions.md#artifacts)
|
|
24
25
|
- **Share a chat or a page.** A read-only snapshot link with secrets, home paths and machine names masked, a preview before anything leaves your machine, and a Markdown copy to hand to an agent. [More](docs/guides/sharing.md)
|
|
25
26
|
- **Project config.** `agentos.json` gives each session its own ports, environment, files and a private Postgres copy. [More](docs/reference/project-config.md)
|
|
26
27
|
- **Schedules and recipes.** Start work on a timer, or save a job that comes up again. [More](docs/guides/schedules.md)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { peerLinkOf } from "@/lib/hosts/peer-actions";
|
|
3
|
+
import { collectArtifacts, getSession } from "@/lib/session-artifacts/collect";
|
|
4
|
+
import { peerContent } from "@/lib/session-artifacts/peer";
|
|
5
|
+
import { artifactContent } from "@/lib/session-artifacts/serve";
|
|
6
|
+
|
|
7
|
+
// GET /api/sessions/[id]/artifacts/[artifactId] - one artifact's content.
|
|
8
|
+
// By id only: a path the client sends is never read.
|
|
9
|
+
export async function GET(
|
|
10
|
+
_request: Request,
|
|
11
|
+
{ params }: { params: Promise<{ id: string; artifactId: string }> }
|
|
12
|
+
) {
|
|
13
|
+
const { id, artifactId } = await params;
|
|
14
|
+
const session = getSession(id);
|
|
15
|
+
if (!session)
|
|
16
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
17
|
+
const link = peerLinkOf(session);
|
|
18
|
+
const content = link
|
|
19
|
+
? await peerContent(link, id, artifactId)
|
|
20
|
+
: await (async () => {
|
|
21
|
+
const entry = (await collectArtifacts(session)).find(
|
|
22
|
+
(e) => e.artifact.id === artifactId
|
|
23
|
+
);
|
|
24
|
+
return entry
|
|
25
|
+
? artifactContent(session, entry)
|
|
26
|
+
: ({ ok: false, status: 404, error: "Not found" } as const);
|
|
27
|
+
})();
|
|
28
|
+
if (!content.ok)
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ error: content.error },
|
|
31
|
+
{ status: content.status }
|
|
32
|
+
);
|
|
33
|
+
return new NextResponse(content.body, { headers: content.headers });
|
|
34
|
+
}
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
2
|
+
import { peerLinkOf } from "@/lib/hosts/peer-actions";
|
|
3
|
+
import { collectArtifacts, getSession } from "@/lib/session-artifacts/collect";
|
|
4
|
+
import { peerArtifacts } from "@/lib/session-artifacts/peer";
|
|
3
5
|
|
|
4
|
-
// GET /api/sessions/[id]/artifacts - the
|
|
6
|
+
// GET /api/sessions/[id]/artifacts - everything the session made: its plan,
|
|
7
|
+
// files, pages, images, job logs, PR, recipe run and log
|
|
5
8
|
export async function GET(
|
|
6
9
|
_request: Request,
|
|
7
10
|
{ params }: { params: Promise<{ id: string }> }
|
|
8
11
|
) {
|
|
9
12
|
const { id } = await params;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const session = getSession(id);
|
|
14
|
+
if (!session)
|
|
15
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
16
|
+
const link = peerLinkOf(session);
|
|
17
|
+
if (link) {
|
|
18
|
+
try {
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
artifacts: await peerArtifacts(link, id),
|
|
21
|
+
host: link.hostName,
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
artifacts: [],
|
|
26
|
+
host: link.hostName,
|
|
27
|
+
unreachable: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const entries = await collectArtifacts(session);
|
|
32
|
+
return NextResponse.json({ artifacts: entries.map((e) => e.artifact) });
|
|
17
33
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Check,
|
|
6
|
+
ChevronLeft,
|
|
7
|
+
Copy,
|
|
8
|
+
ExternalLink,
|
|
9
|
+
FolderOpen,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Markdown } from "@/components/Chat/Markdown";
|
|
13
|
+
import { ArtifactFrame } from "@/components/Chat/ArtifactFrame";
|
|
14
|
+
import { artifactContentUrl, useArtifactText } from "@/data/session-artifacts";
|
|
15
|
+
import { formatBytes } from "@/lib/share/view";
|
|
16
|
+
import { relativeTime } from "@/lib/schedules/when";
|
|
17
|
+
import { openable, type SessionArtifact } from "@/lib/session-artifacts/types";
|
|
18
|
+
import { cn } from "@/lib/utils";
|
|
19
|
+
import { ArtifactIcon } from "./ArtifactRow";
|
|
20
|
+
import { stateLabel } from "./helpers";
|
|
21
|
+
|
|
22
|
+
function Body({
|
|
23
|
+
sessionId,
|
|
24
|
+
artifact,
|
|
25
|
+
pr,
|
|
26
|
+
}: {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
artifact: SessionArtifact;
|
|
29
|
+
pr: string | null;
|
|
30
|
+
}) {
|
|
31
|
+
const url = artifactContentUrl(sessionId, artifact.id);
|
|
32
|
+
const textual =
|
|
33
|
+
openable(artifact) &&
|
|
34
|
+
(artifact.preview === "markdown" || artifact.preview === "text");
|
|
35
|
+
const text = useArtifactText(textual ? url : null);
|
|
36
|
+
|
|
37
|
+
if (!openable(artifact))
|
|
38
|
+
return (
|
|
39
|
+
<p className="text-muted-foreground p-6 text-sm">
|
|
40
|
+
{artifact.state === "in-pr"
|
|
41
|
+
? `Committed, and its worktree was removed: it's in ${pr ? `PR #${pr}` : "the PR"}.`
|
|
42
|
+
: artifact.state === "outside"
|
|
43
|
+
? "Outside the folders AgentOS serves, so it isn't opened here."
|
|
44
|
+
: artifact.state === "gone"
|
|
45
|
+
? "Gone: its worktree was removed and no copy was kept."
|
|
46
|
+
: "Nothing to preview."}
|
|
47
|
+
</p>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (artifact.preview === "html")
|
|
51
|
+
return (
|
|
52
|
+
<ArtifactFrame
|
|
53
|
+
artifactId={artifact.id}
|
|
54
|
+
src={url}
|
|
55
|
+
title={artifact.name}
|
|
56
|
+
fill
|
|
57
|
+
className="bg-background"
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (artifact.preview === "image")
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex min-h-full items-start justify-center p-4">
|
|
64
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
65
|
+
<img
|
|
66
|
+
src={url}
|
|
67
|
+
alt={artifact.name}
|
|
68
|
+
className="max-w-full rounded-lg border"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (text.isPending)
|
|
74
|
+
return <div className="bg-muted/40 m-4 h-40 animate-pulse rounded-lg" />;
|
|
75
|
+
if (text.isError)
|
|
76
|
+
return <p className="text-destructive p-4 text-sm">{text.error.message}</p>;
|
|
77
|
+
const note = text.data.truncated && (
|
|
78
|
+
<p className="text-muted-foreground mb-3 text-xs">
|
|
79
|
+
{artifact.kind === "log"
|
|
80
|
+
? "Showing the end of the output."
|
|
81
|
+
: "Showing the start of the file."}
|
|
82
|
+
</p>
|
|
83
|
+
);
|
|
84
|
+
if (artifact.preview === "markdown")
|
|
85
|
+
return (
|
|
86
|
+
<div className="mx-auto max-w-3xl px-5 py-5">
|
|
87
|
+
{note}
|
|
88
|
+
<Markdown text={text.data.text} />
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
return (
|
|
92
|
+
<div className="p-4">
|
|
93
|
+
{note}
|
|
94
|
+
<pre className="font-mono text-xs leading-relaxed break-words whitespace-pre-wrap">
|
|
95
|
+
{text.data.text}
|
|
96
|
+
</pre>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ArtifactPreview({
|
|
102
|
+
sessionId,
|
|
103
|
+
artifact,
|
|
104
|
+
pr,
|
|
105
|
+
mobile,
|
|
106
|
+
onBack,
|
|
107
|
+
onOpenInFiles,
|
|
108
|
+
}: {
|
|
109
|
+
sessionId: string;
|
|
110
|
+
artifact: SessionArtifact;
|
|
111
|
+
pr: string | null;
|
|
112
|
+
mobile: boolean;
|
|
113
|
+
onBack?: () => void;
|
|
114
|
+
onOpenInFiles?: (path: string) => void;
|
|
115
|
+
}) {
|
|
116
|
+
const [copied, setCopied] = useState(false);
|
|
117
|
+
const label = stateLabel(artifact, pr);
|
|
118
|
+
// Inside the worktree, the path from its top; the full one is copied.
|
|
119
|
+
const where =
|
|
120
|
+
artifact.path && artifact.folder !== undefined
|
|
121
|
+
? [artifact.folder, artifact.path.split("/").pop()]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join("/")
|
|
124
|
+
: artifact.path;
|
|
125
|
+
const meta = [
|
|
126
|
+
where,
|
|
127
|
+
label,
|
|
128
|
+
artifact.size !== undefined ? formatBytes(artifact.size) : null,
|
|
129
|
+
mobile && artifact.at ? relativeTime(artifact.at) : null,
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(" · ");
|
|
133
|
+
const copy = () => {
|
|
134
|
+
if (!artifact.path) return;
|
|
135
|
+
void navigator.clipboard?.writeText(artifact.path).then(() => {
|
|
136
|
+
setCopied(true);
|
|
137
|
+
setTimeout(() => setCopied(false), 1500);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const canOpenInFiles =
|
|
141
|
+
!!onOpenInFiles &&
|
|
142
|
+
!!artifact.path &&
|
|
143
|
+
artifact.state === "live" &&
|
|
144
|
+
(artifact.kind === "file" || artifact.kind === "plan");
|
|
145
|
+
const actions = (
|
|
146
|
+
<>
|
|
147
|
+
{artifact.url && (
|
|
148
|
+
<Button
|
|
149
|
+
variant="outline"
|
|
150
|
+
size="sm"
|
|
151
|
+
asChild
|
|
152
|
+
className={cn(mobile && "h-11 flex-1")}
|
|
153
|
+
>
|
|
154
|
+
<a
|
|
155
|
+
href={artifact.url}
|
|
156
|
+
target="_blank"
|
|
157
|
+
rel="noreferrer"
|
|
158
|
+
title="Open PR"
|
|
159
|
+
>
|
|
160
|
+
<ExternalLink className="h-3.5 w-3.5" />{" "}
|
|
161
|
+
<span className={cn(!mobile && "hidden @lg:inline")}>Open PR</span>
|
|
162
|
+
</a>
|
|
163
|
+
</Button>
|
|
164
|
+
)}
|
|
165
|
+
{artifact.path && (
|
|
166
|
+
<Button
|
|
167
|
+
variant="outline"
|
|
168
|
+
size="sm"
|
|
169
|
+
onClick={copy}
|
|
170
|
+
title="Copy path"
|
|
171
|
+
className={cn(mobile && "h-11 flex-1")}
|
|
172
|
+
>
|
|
173
|
+
{copied ? (
|
|
174
|
+
<Check className="h-3.5 w-3.5" />
|
|
175
|
+
) : (
|
|
176
|
+
<Copy className="h-3.5 w-3.5" />
|
|
177
|
+
)}
|
|
178
|
+
<span className={cn(!mobile && "hidden @lg:inline")}>Copy path</span>
|
|
179
|
+
</Button>
|
|
180
|
+
)}
|
|
181
|
+
{canOpenInFiles && (
|
|
182
|
+
<Button
|
|
183
|
+
variant="outline"
|
|
184
|
+
size="sm"
|
|
185
|
+
onClick={() => onOpenInFiles(artifact.path!)}
|
|
186
|
+
title="Open in Files"
|
|
187
|
+
className={cn(mobile && "h-11 flex-1")}
|
|
188
|
+
>
|
|
189
|
+
<FolderOpen className="h-3.5 w-3.5" />{" "}
|
|
190
|
+
<span className={cn(!mobile && "hidden @lg:inline")}>
|
|
191
|
+
Open in Files
|
|
192
|
+
</span>
|
|
193
|
+
</Button>
|
|
194
|
+
)}
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
const hasActions = !!(artifact.url || artifact.path);
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="@container flex h-full min-w-0 flex-col">
|
|
201
|
+
<div className="flex items-center gap-2.5 border-b px-3 py-2.5 sm:px-4">
|
|
202
|
+
{mobile ? (
|
|
203
|
+
<Button
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="icon"
|
|
206
|
+
aria-label="Back to artifacts"
|
|
207
|
+
onClick={onBack}
|
|
208
|
+
className="-ml-1 h-11 w-11 shrink-0"
|
|
209
|
+
>
|
|
210
|
+
<ChevronLeft className="h-5 w-5" />
|
|
211
|
+
</Button>
|
|
212
|
+
) : (
|
|
213
|
+
<ArtifactIcon artifact={artifact} />
|
|
214
|
+
)}
|
|
215
|
+
<div className="min-w-0 flex-1">
|
|
216
|
+
<div className="truncate text-[13px] font-medium">
|
|
217
|
+
{artifact.name}
|
|
218
|
+
</div>
|
|
219
|
+
{meta && (
|
|
220
|
+
<div className="text-muted-foreground truncate font-mono text-[11px]">
|
|
221
|
+
{meta}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
{!mobile && hasActions && (
|
|
226
|
+
<div className="flex shrink-0 gap-1.5">{actions}</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
230
|
+
<Body sessionId={sessionId} artifact={artifact} pr={pr} />
|
|
231
|
+
</div>
|
|
232
|
+
{mobile && hasActions && (
|
|
233
|
+
<div className="flex gap-2 border-t px-3 pt-2.5 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
|
|
234
|
+
{actions}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AppWindow,
|
|
5
|
+
FileCode2,
|
|
6
|
+
FileText,
|
|
7
|
+
GitPullRequest,
|
|
8
|
+
Image as ImageIcon,
|
|
9
|
+
ListChecks,
|
|
10
|
+
Play,
|
|
11
|
+
ScrollText,
|
|
12
|
+
SquareTerminal,
|
|
13
|
+
type LucideIcon,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
import { formatBytes } from "@/lib/share/view";
|
|
17
|
+
import { relativeTime } from "@/lib/schedules/when";
|
|
18
|
+
import type {
|
|
19
|
+
ArtifactKind,
|
|
20
|
+
SessionArtifact,
|
|
21
|
+
} from "@/lib/session-artifacts/types";
|
|
22
|
+
import { folderOf, isGone, stateLabel } from "./helpers";
|
|
23
|
+
|
|
24
|
+
const ICONS: Record<ArtifactKind, LucideIcon> = {
|
|
25
|
+
plan: ListChecks,
|
|
26
|
+
pr: GitPullRequest,
|
|
27
|
+
run: Play,
|
|
28
|
+
"session-log": ScrollText,
|
|
29
|
+
page: AppWindow,
|
|
30
|
+
file: FileText,
|
|
31
|
+
image: ImageIcon,
|
|
32
|
+
log: SquareTerminal,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function ArtifactIcon({ artifact }: { artifact: SessionArtifact }) {
|
|
36
|
+
const Icon =
|
|
37
|
+
artifact.kind === "file" && artifact.preview === "html"
|
|
38
|
+
? FileCode2
|
|
39
|
+
: ICONS[artifact.kind];
|
|
40
|
+
const lead =
|
|
41
|
+
artifact.kind === "plan" ||
|
|
42
|
+
artifact.kind === "pr" ||
|
|
43
|
+
artifact.kind === "run";
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
className={cn(
|
|
47
|
+
"bg-muted flex h-7 w-7 shrink-0 items-center justify-center rounded-md",
|
|
48
|
+
lead ? "text-primary" : "text-muted-foreground"
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<Icon className="h-3.5 w-3.5" />
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MONO: ArtifactKind[] = ["file", "image"];
|
|
57
|
+
|
|
58
|
+
// The line under the name: where it is, or what it is.
|
|
59
|
+
function subline(a: SessionArtifact, mobile: boolean): string {
|
|
60
|
+
const bits: string[] = [];
|
|
61
|
+
if (a.by === "you") bits.push("attached by you");
|
|
62
|
+
if (a.detail) bits.push(a.detail);
|
|
63
|
+
if (mobile && a.edits)
|
|
64
|
+
bits.push(`${a.edits} edit${a.edits === 1 ? "" : "s"}`);
|
|
65
|
+
if (!bits.length && a.folder !== undefined) return a.folder;
|
|
66
|
+
if (!bits.length && a.path) return folderOf(a.path);
|
|
67
|
+
if (a.by === "shell") bits.push("made by a command");
|
|
68
|
+
return bits.join(" · ");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ArtifactRow({
|
|
72
|
+
artifact,
|
|
73
|
+
pr,
|
|
74
|
+
selected,
|
|
75
|
+
mobile,
|
|
76
|
+
onSelect,
|
|
77
|
+
}: {
|
|
78
|
+
artifact: SessionArtifact;
|
|
79
|
+
pr: string | null;
|
|
80
|
+
selected: boolean;
|
|
81
|
+
mobile: boolean;
|
|
82
|
+
onSelect: () => void;
|
|
83
|
+
}) {
|
|
84
|
+
const label = stateLabel(artifact, pr);
|
|
85
|
+
const size = artifact.size !== undefined ? formatBytes(artifact.size) : null;
|
|
86
|
+
const when = artifact.at ? relativeTime(artifact.at) : null;
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
data-artifact-id={artifact.id}
|
|
91
|
+
onClick={onSelect}
|
|
92
|
+
aria-current={selected || undefined}
|
|
93
|
+
className={cn(
|
|
94
|
+
"hover:bg-accent/60 flex w-full items-center gap-2.5 px-3.5 py-2 text-left transition-colors",
|
|
95
|
+
mobile ? "min-h-14" : "min-h-12",
|
|
96
|
+
selected && "bg-accent shadow-[inset_2px_0_0_hsl(var(--primary))]",
|
|
97
|
+
isGone(artifact) && "opacity-55"
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<ArtifactIcon artifact={artifact} />
|
|
101
|
+
<span className="min-w-0 flex-1">
|
|
102
|
+
<span
|
|
103
|
+
className={cn(
|
|
104
|
+
"block truncate text-[13px]",
|
|
105
|
+
MONO.includes(artifact.kind) && artifact.by !== "you" && "font-mono"
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
{artifact.name}
|
|
109
|
+
</span>
|
|
110
|
+
<span
|
|
111
|
+
className={cn(
|
|
112
|
+
"text-muted-foreground block truncate text-[11.5px]",
|
|
113
|
+
!artifact.detail && artifact.path && "font-mono"
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{subline(artifact, mobile)}
|
|
117
|
+
</span>
|
|
118
|
+
</span>
|
|
119
|
+
<span className="text-muted-foreground shrink-0 text-right text-[11.5px] whitespace-nowrap">
|
|
120
|
+
{!mobile && (artifact.edits || label) && (
|
|
121
|
+
<span className="block">
|
|
122
|
+
{artifact.edits
|
|
123
|
+
? `${artifact.edits} edit${artifact.edits === 1 ? "" : "s"}`
|
|
124
|
+
: null}
|
|
125
|
+
{label && (
|
|
126
|
+
<span
|
|
127
|
+
className={cn(
|
|
128
|
+
"ml-1 rounded border px-1 text-[10.5px]",
|
|
129
|
+
artifact.state === "kept" &&
|
|
130
|
+
"border-emerald-500/35 text-emerald-600 dark:text-emerald-400",
|
|
131
|
+
artifact.state === "in-pr" && "border-primary/35 text-primary"
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{label}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
<span className="block">
|
|
140
|
+
{mobile && label ? `${label} · ` : ""}
|
|
141
|
+
{[size, when].filter(Boolean).join(" · ")}
|
|
142
|
+
{mobile && " ›"}
|
|
143
|
+
</span>
|
|
144
|
+
</span>
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
}
|