feat: wire real AgentOS execution
This commit is contained in:
@@ -5,8 +5,10 @@ import {
|
||||
} from "@code/ui/components/ai-elements/conversation";
|
||||
import { Button } from "@code/ui/components/button";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronRight,
|
||||
Check,
|
||||
FileCode2,
|
||||
FolderGit2,
|
||||
Hammer,
|
||||
LoaderCircle,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
ImagePlus,
|
||||
Play,
|
||||
RotateCcw,
|
||||
ScrollText,
|
||||
Send,
|
||||
Sparkles,
|
||||
Settings,
|
||||
@@ -36,6 +39,28 @@ import {
|
||||
type SliceWork = NonNullable<ReturnType<typeof useSliceOne>["works"]>[number];
|
||||
const EMPTY_WORKS: readonly SliceWork[] = [];
|
||||
|
||||
type SliceArtifact = NonNullable<
|
||||
NonNullable<SliceWork["runs"][number]["artifacts"]>[number]
|
||||
>;
|
||||
|
||||
interface ArtifactMetadata {
|
||||
readonly changedFiles?: readonly string[];
|
||||
}
|
||||
|
||||
const parseArtifactMetadata = (artifact: SliceArtifact): ArtifactMetadata => {
|
||||
if (!artifact.metadataJson) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(artifact.metadataJson) as ArtifactMetadata;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const changedFilesFor = (artifact: SliceArtifact): readonly string[] =>
|
||||
parseArtifactMetadata(artifact).changedFiles ?? [];
|
||||
|
||||
interface WorkCardProps {
|
||||
readonly onSourceSelect: (rawText: string) => void;
|
||||
readonly work: SliceWork;
|
||||
@@ -96,6 +121,7 @@ const starterDesign = (work: SliceWork) => ({
|
||||
const WorkCard = ({ onSourceSelect, work, slice }: WorkCardProps) => {
|
||||
const [sourcesOpen, setSourcesOpen] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
const sources = work.signals.flatMap((signal) => signal.sources);
|
||||
const { definition } = work;
|
||||
const { design } = work;
|
||||
@@ -258,6 +284,22 @@ const WorkCard = ({ onSourceSelect, work, slice }: WorkCardProps) => {
|
||||
<p className="font-semibold uppercase tracking-wide text-[#65713a]">
|
||||
Build
|
||||
</p>
|
||||
{slice.operationError ? (
|
||||
<p className="mt-2 flex items-start gap-1.5 border border-red-300 bg-red-50 px-2 py-1.5 text-red-800">
|
||||
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 leading-5">
|
||||
{slice.operationError.message}
|
||||
</span>
|
||||
<button
|
||||
aria-label="Dismiss error"
|
||||
className="shrink-0"
|
||||
onClick={() => slice.clearOperationError()}
|
||||
type="button"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</p>
|
||||
) : null}
|
||||
{latestRun ? (
|
||||
<div className="mt-1 space-y-2 text-[#626057]">
|
||||
<p className="leading-5">
|
||||
@@ -269,31 +311,75 @@ const WorkCard = ({ onSourceSelect, work, slice }: WorkCardProps) => {
|
||||
latestRun.terminalClassification ??
|
||||
"activity is still arriving"}
|
||||
</p>
|
||||
{latestRun.attemptEvents?.slice(-5).map((item) => (
|
||||
<p
|
||||
className="border-l-2 border-[#b8c760] pl-2"
|
||||
key={item._id}
|
||||
>
|
||||
{item.message}
|
||||
</p>
|
||||
))}
|
||||
{latestRun.baseRevision ? (
|
||||
<p className="font-mono text-[10px] text-[#747168]">
|
||||
{latestRun.baseRevision.slice(0, 8)} →{" "}
|
||||
{latestRun.candidateRevision?.slice(0, 8) ?? "working"}
|
||||
</p>
|
||||
) : null}
|
||||
{latestRun.artifacts?.map((artifact) => (
|
||||
<a
|
||||
className="block font-medium underline"
|
||||
href={artifact.uri}
|
||||
key={artifact._id}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{artifact.title}
|
||||
</a>
|
||||
))}
|
||||
{latestRun.artifacts?.map((artifact) => {
|
||||
const changedFiles = changedFilesFor(artifact);
|
||||
return (
|
||||
<div key={artifact._id} className="space-y-1">
|
||||
{artifact.uri ? (
|
||||
<a
|
||||
className="block font-medium underline"
|
||||
href={artifact.uri}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{artifact.title}
|
||||
</a>
|
||||
) : (
|
||||
<p className="font-medium">{artifact.title}</p>
|
||||
)}
|
||||
{changedFiles.length > 0 ? (
|
||||
<ul className="space-y-0.5">
|
||||
{changedFiles.map((file) => (
|
||||
<li
|
||||
className="flex items-start gap-1.5 font-mono text-[11px] leading-5 text-[#747168]"
|
||||
key={file}
|
||||
>
|
||||
<FileCode2 className="mt-0.5 size-3 shrink-0 text-[#9a985f]" />
|
||||
<span className="min-w-0 break-all">{file}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{latestRun.attemptEvents &&
|
||||
latestRun.attemptEvents.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{(logsOpen
|
||||
? latestRun.attemptEvents
|
||||
: latestRun.attemptEvents.slice(-3)
|
||||
).map((item) => (
|
||||
<p
|
||||
className="border-l-2 border-[#b8c760] pl-2 leading-5"
|
||||
key={item._id}
|
||||
>
|
||||
{item.message}
|
||||
</p>
|
||||
))}
|
||||
{latestRun.attemptEvents.length > 3 ? (
|
||||
<button
|
||||
className="flex items-center gap-1 font-medium text-[#65713a] hover:underline"
|
||||
onClick={() => setLogsOpen((open) => !open)}
|
||||
type="button"
|
||||
>
|
||||
<ScrollText className="size-3.5" />
|
||||
{logsOpen
|
||||
? "Show recent activity"
|
||||
: `Show full activity log (${latestRun.attemptEvents.length})`}
|
||||
<ChevronRight
|
||||
className={`size-3.5 transition-transform ${logsOpen ? "rotate-90" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-1 text-[#747168]">No implementation Run yet.</p>
|
||||
@@ -303,9 +389,7 @@ const WorkCard = ({ onSourceSelect, work, slice }: WorkCardProps) => {
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!slice.projectGitConnection}
|
||||
onClick={() =>
|
||||
void slice.startExecution(work._id, design?.slices?.[0]?.id)
|
||||
}
|
||||
onClick={() => void slice.startExecution(work._id)}
|
||||
>
|
||||
<Play className="size-3.5" /> Run
|
||||
</Button>
|
||||
@@ -315,11 +399,7 @@ const WorkCard = ({ onSourceSelect, work, slice }: WorkCardProps) => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
void slice.startSimulation(
|
||||
work._id,
|
||||
"success",
|
||||
design?.slices?.[0]?.id
|
||||
)
|
||||
void slice.startSimulation(work._id, "success")
|
||||
}
|
||||
>
|
||||
<Play className="size-3.5" /> Simulate
|
||||
@@ -338,7 +418,8 @@ const WorkCard = ({ onSourceSelect, work, slice }: WorkCardProps) => {
|
||||
<X className="size-3.5" /> Cancel
|
||||
</Button>
|
||||
) : null}
|
||||
{latestRun?.status === "terminal" &&
|
||||
{latestRun?.executionKind !== "real" &&
|
||||
latestRun?.status === "terminal" &&
|
||||
latestRun.terminalClassification === "RetryableFailure" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -428,6 +509,7 @@ const ConnectProject = ({ slice }: { slice: SliceOneState }) => (
|
||||
</main>
|
||||
);
|
||||
|
||||
// oxlint-disable-next-line complexity -- this page coordinates the existing mobile shell without owning domain logic.
|
||||
export const SliceOnePage = () => {
|
||||
const slice = useSliceOne();
|
||||
const viewportStyle = useVisualViewportStyle();
|
||||
@@ -553,6 +635,22 @@ export const SliceOnePage = () => {
|
||||
? `${slice.projectGitConnection.provider} · ${slice.projectGitConnection.serverUrl}`
|
||||
: "No Git credentials attached"}
|
||||
</p>
|
||||
{slice.operationError ? (
|
||||
<p className="mt-2 flex items-start gap-1.5 border border-red-300 bg-red-50 px-2 py-1.5 text-xs text-red-800">
|
||||
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 leading-5">
|
||||
{slice.operationError.message}
|
||||
</span>
|
||||
<button
|
||||
aria-label="Dismiss error"
|
||||
className="shrink-0"
|
||||
onClick={() => slice.clearOperationError()}
|
||||
type="button"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -40,7 +40,9 @@ interface WorkRecord {
|
||||
_id: string;
|
||||
kind: string;
|
||||
message: string;
|
||||
metadataJson: string;
|
||||
occurredAt: number;
|
||||
sequence: number;
|
||||
}[];
|
||||
readonly baseRevision?: string;
|
||||
readonly candidateRevision?: string;
|
||||
@@ -179,6 +181,20 @@ export const useSliceOne = () => {
|
||||
const [repository, setRepository] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [operationError, setOperationError] = useState<Error>();
|
||||
const captureOperationError =
|
||||
<TArgs extends unknown[], TResult>(
|
||||
operation: (...args: TArgs) => Promise<TResult>
|
||||
) =>
|
||||
async (...args: TArgs): Promise<TResult> => {
|
||||
setOperationError(undefined);
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (caughtError) {
|
||||
setOperationError(toError(caughtError));
|
||||
throw caughtError;
|
||||
}
|
||||
};
|
||||
const selectedProjectStillExists = projects?.some(
|
||||
(project) => project.id === (selectedProjectId as unknown as string)
|
||||
);
|
||||
@@ -262,15 +278,18 @@ export const useSliceOne = () => {
|
||||
| "permanent-failure"
|
||||
| "cancelled",
|
||||
sliceId?: string
|
||||
) => startSimulationMutation({ scenario, sliceId, workId });
|
||||
) =>
|
||||
captureOperationError(() =>
|
||||
startSimulationMutation({ scenario, sliceId, workId })
|
||||
)();
|
||||
const cancelSimulation = (runId: Id<"workRuns">) =>
|
||||
cancelSimulationMutation({ runId });
|
||||
captureOperationError(() => cancelSimulationMutation({ runId }))();
|
||||
const retrySimulation = (runId: Id<"workRuns">) =>
|
||||
retrySimulationMutation({ runId });
|
||||
captureOperationError(() => retrySimulationMutation({ runId }))();
|
||||
const startExecution = (workId: Id<"works">, sliceId?: string) =>
|
||||
startExecutionMutation({ sliceId, workId });
|
||||
captureOperationError(() => startExecutionMutation({ sliceId, workId }))();
|
||||
const cancelExecution = (runId: Id<"workRuns">) =>
|
||||
cancelExecutionMutation({ runId });
|
||||
captureOperationError(() => cancelExecutionMutation({ runId }))();
|
||||
const attachConnection = async (connectionId: Id<"gitConnections">) => {
|
||||
if (!activeProjectId) {
|
||||
throw new Error("Select a project first");
|
||||
@@ -280,18 +299,16 @@ export const useSliceOne = () => {
|
||||
projectId: activeProjectId,
|
||||
});
|
||||
};
|
||||
const connectGitea = async (input: {
|
||||
serverUrl: string;
|
||||
token: string;
|
||||
username?: string;
|
||||
}) => {
|
||||
const result = await connectGiteaAction(input);
|
||||
await attachConnection(result.connectionId);
|
||||
};
|
||||
const connectLinkedGithub = async () => {
|
||||
const connectGitea = captureOperationError(
|
||||
async (input: { serverUrl: string; token: string; username?: string }) => {
|
||||
const result = await connectGiteaAction(input);
|
||||
await attachConnection(result.connectionId);
|
||||
}
|
||||
);
|
||||
const connectLinkedGithub = captureOperationError(async () => {
|
||||
const result = await connectGithubAction({});
|
||||
await attachConnection(result.connectionId);
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
agent,
|
||||
@@ -300,11 +317,13 @@ export const useSliceOne = () => {
|
||||
authorizeGithub,
|
||||
cancelExecution,
|
||||
cancelSimulation,
|
||||
clearOperationError: () => setOperationError(undefined),
|
||||
connectGitea,
|
||||
connectLinkedGithub,
|
||||
connectRepository,
|
||||
error,
|
||||
gitConnections,
|
||||
operationError,
|
||||
pending,
|
||||
projectGitConnection,
|
||||
projects,
|
||||
|
||||
Reference in New Issue
Block a user