mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
Square execution workspace configuration
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
dace1bc58e
commit
7b686b2678
3 files changed
+76
-27
No files matched your search
@@ -237,6 +237,42 @@ describe("WorkspaceRuntimeControls", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("can render square plain surfaces for embedded configuration pages", () => {
|
||||
const sections = buildWorkspaceRuntimeControlSections({
|
||||
runtimeConfig: {
|
||||
commands: [
|
||||
{ id: "web", name: "web", kind: "service", command: "pnpm dev" },
|
||||
],
|
||||
},
|
||||
runtimeServices: [],
|
||||
canStartServices: true,
|
||||
});
|
||||
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(
|
||||
<WorkspaceRuntimeControls
|
||||
sections={sections}
|
||||
square
|
||||
onAction={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const summaryPanel = container.querySelector(".border.border-border\\/70");
|
||||
const servicePanel = Array.from(container.querySelectorAll(".border.border-border\\/80"))
|
||||
.find((element) => element.textContent?.includes("web"));
|
||||
const startButton = Array.from(container.querySelectorAll("button"))
|
||||
.find((button) => button.textContent?.trim() === "Start");
|
||||
|
||||
expect(summaryPanel?.className).toContain("rounded-none");
|
||||
expect(summaryPanel?.className).not.toContain("bg-background/60");
|
||||
expect(servicePanel?.className).toContain("rounded-none");
|
||||
expect(startButton?.className).toContain("rounded-none");
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("accepts the legacy items prop without crashing", () => {
|
||||
const items = buildWorkspaceRuntimeControlItems({
|
||||
runtimeConfig: {
|
||||
|
||||
@@ -57,6 +57,7 @@ type WorkspaceRuntimeControlsProps = {
|
||||
disabledHint?: string | null;
|
||||
onAction: (request: WorkspaceRuntimeControlRequest) => void;
|
||||
className?: string;
|
||||
square?: boolean;
|
||||
} | {
|
||||
sections?: never;
|
||||
items: LegacyWorkspaceRuntimeControlItem[];
|
||||
@@ -68,6 +69,7 @@ type WorkspaceRuntimeControlsProps = {
|
||||
disabledHint?: string | null;
|
||||
onAction: (request: WorkspaceRuntimeControlRequest) => void;
|
||||
className?: string;
|
||||
square?: boolean;
|
||||
};
|
||||
|
||||
export function hasRunningRuntimeServices(
|
||||
@@ -212,11 +214,13 @@ function CommandActionButtons({
|
||||
isPending,
|
||||
pendingRequest,
|
||||
onAction,
|
||||
square,
|
||||
}: {
|
||||
item: WorkspaceRuntimeControlItem;
|
||||
isPending: boolean;
|
||||
pendingRequest: WorkspaceRuntimeControlRequest | null | undefined;
|
||||
onAction: (request: WorkspaceRuntimeControlRequest) => void;
|
||||
square?: boolean;
|
||||
}) {
|
||||
const actions: WorkspaceRuntimeAction[] =
|
||||
item.kind === "job"
|
||||
@@ -249,7 +253,8 @@ function CommandActionButtons({
|
||||
variant={action === "stop" ? "destructive" : action === "restart" ? "outline" : "default"}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-9 w-full justify-start rounded-xl px-3 shadow-none sm:w-auto",
|
||||
"h-9 w-full justify-start px-3 shadow-none sm:w-auto",
|
||||
square ? "rounded-none" : "rounded-xl",
|
||||
action === "restart" ? "bg-background" : null,
|
||||
)}
|
||||
disabled={disabled}
|
||||
@@ -273,6 +278,7 @@ function CommandSection({
|
||||
isPending,
|
||||
pendingRequest,
|
||||
onAction,
|
||||
square,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -282,6 +288,7 @@ function CommandSection({
|
||||
isPending: boolean;
|
||||
pendingRequest: WorkspaceRuntimeControlRequest | null | undefined;
|
||||
onAction: (request: WorkspaceRuntimeControlRequest) => void;
|
||||
square?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -290,14 +297,14 @@ function CommandSection({
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border/80 bg-background/50 px-3 py-4 text-sm text-muted-foreground">
|
||||
<div className={cn("border border-dashed border-border/80 bg-background px-3 py-4 text-sm text-muted-foreground", square ? "rounded-none" : "rounded-xl")}>
|
||||
{emptyMessage}
|
||||
{disabledHint ? <p className="mt-2 text-xs">{disabledHint}</p> : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{items.map((item) => (
|
||||
<div key={item.key} className="rounded-xl border border-border/80 bg-background px-3 py-3">
|
||||
<div key={item.key} className={cn("border border-border/80 bg-background px-3 py-3", square ? "rounded-none" : "rounded-xl")}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
@@ -312,6 +319,7 @@ function CommandSection({
|
||||
isPending={isPending}
|
||||
pendingRequest={pendingRequest}
|
||||
onAction={onAction}
|
||||
square={square}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
@@ -360,6 +368,7 @@ export function WorkspaceRuntimeControls({
|
||||
disabledHint = null,
|
||||
onAction,
|
||||
className,
|
||||
square,
|
||||
}: WorkspaceRuntimeControlsProps) {
|
||||
const resolvedSections = sections ?? {
|
||||
services: (items ?? []).map((item) => ({
|
||||
@@ -377,7 +386,7 @@ export function WorkspaceRuntimeControls({
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
<div className="rounded-xl border border-border/70 bg-background/60 p-3">
|
||||
<div className={cn("border border-border/70 bg-background p-3", square ? "rounded-none" : "rounded-xl")}>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Workspace commands</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -411,6 +420,7 @@ export function WorkspaceRuntimeControls({
|
||||
isPending={isPending}
|
||||
pendingRequest={pendingRequest}
|
||||
onAction={onAction}
|
||||
square={square}
|
||||
/>
|
||||
|
||||
<CommandSection
|
||||
@@ -421,6 +431,7 @@ export function WorkspaceRuntimeControls({
|
||||
isPending={isPending}
|
||||
pendingRequest={pendingRequest}
|
||||
onAction={onAction}
|
||||
square={square}
|
||||
/>
|
||||
|
||||
{resolvedSections.otherServices.length > 0 ? (
|
||||
@@ -432,6 +443,7 @@ export function WorkspaceRuntimeControls({
|
||||
isPending={isPending}
|
||||
pendingRequest={pendingRequest}
|
||||
onAction={onAction}
|
||||
square={square}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -532,7 +532,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-border bg-card p-4 sm:p-5">
|
||||
<div className="border border-border bg-background p-4 sm:p-5">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Workspace commands</div>
|
||||
@@ -548,6 +548,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
</div>
|
||||
<WorkspaceRuntimeControls
|
||||
className="mt-4"
|
||||
square
|
||||
sections={runtimeControlSections}
|
||||
isPending={controlRuntimeServices.isPending}
|
||||
pendingRequest={pendingRuntimeAction}
|
||||
@@ -583,7 +584,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
{activeTab === "configuration" ? (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
<div className="rounded-2xl border border-border bg-card p-4 sm:p-5">
|
||||
<div className="border border-border bg-background p-4 sm:p-5">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
|
||||
@@ -595,8 +596,8 @@ export function ExecutionWorkspaceDetail() {
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
variant="destructive"
|
||||
className="w-full rounded-none sm:w-auto"
|
||||
onClick={() => setCloseDialogOpen(true)}
|
||||
disabled={workspace.status === "archived"}
|
||||
>
|
||||
@@ -609,7 +610,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
<div className="space-y-4">
|
||||
<Field label="Workspace name">
|
||||
<input
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none"
|
||||
className="w-full rounded-none border border-border bg-background px-3 py-2 text-sm outline-none"
|
||||
value={form.name}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, name: event.target.value } : current)}
|
||||
placeholder="Execution workspace name"
|
||||
@@ -618,7 +619,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Branch name" hint="Useful for isolated worktrees">
|
||||
<input
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
className="w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
value={form.branchName}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, branchName: event.target.value } : current)}
|
||||
placeholder="PAP-946-workspace"
|
||||
@@ -627,7 +628,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Working directory">
|
||||
<input
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
className="w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
value={form.cwd}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, cwd: event.target.value } : current)}
|
||||
placeholder="/absolute/path/to/workspace"
|
||||
@@ -636,7 +637,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Provider path / ref">
|
||||
<input
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
className="w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
value={form.providerRef}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, providerRef: event.target.value } : current)}
|
||||
placeholder="/path/to/worktree or provider ref"
|
||||
@@ -645,7 +646,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Repo URL">
|
||||
<input
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none"
|
||||
className="w-full rounded-none border border-border bg-background px-3 py-2 text-sm outline-none"
|
||||
value={form.repoUrl}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, repoUrl: event.target.value } : current)}
|
||||
placeholder="https://github.com/org/repo"
|
||||
@@ -654,7 +655,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Base ref">
|
||||
<input
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
className="w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none"
|
||||
value={form.baseRef}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, baseRef: event.target.value } : current)}
|
||||
placeholder="origin/main"
|
||||
@@ -663,7 +664,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Provision command" hint="Runs when Paperclip prepares this execution workspace">
|
||||
<textarea
|
||||
className="min-h-20 w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none sm:min-h-28"
|
||||
className="min-h-20 w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none sm:min-h-28"
|
||||
value={form.provisionCommand}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, provisionCommand: event.target.value } : current)}
|
||||
placeholder="bash ./scripts/provision-worktree.sh"
|
||||
@@ -672,7 +673,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Teardown command" hint="Runs when the execution workspace is archived or cleaned up">
|
||||
<textarea
|
||||
className="min-h-20 w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none sm:min-h-28"
|
||||
className="min-h-20 w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none sm:min-h-28"
|
||||
value={form.teardownCommand}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, teardownCommand: event.target.value } : current)}
|
||||
placeholder="bash ./scripts/teardown-worktree.sh"
|
||||
@@ -681,14 +682,14 @@ export function ExecutionWorkspaceDetail() {
|
||||
|
||||
<Field label="Cleanup command" hint="Workspace-specific cleanup before teardown">
|
||||
<textarea
|
||||
className="min-h-16 w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none sm:min-h-24"
|
||||
className="min-h-16 w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none sm:min-h-24"
|
||||
value={form.cleanupCommand}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, cleanupCommand: event.target.value } : current)}
|
||||
placeholder="pkill -f vite || true"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="rounded-xl border border-dashed border-border/70 bg-muted/20 px-3 py-3">
|
||||
<div className="border border-dashed border-border/70 bg-background px-3 py-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
|
||||
@@ -704,7 +705,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
className="w-full rounded-none sm:w-auto"
|
||||
size="sm"
|
||||
disabled={!linkedProjectWorkspace?.runtimeConfig?.workspaceRuntime}
|
||||
onClick={() =>
|
||||
@@ -720,7 +721,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details className="rounded-xl border border-dashed border-border/70 bg-muted/20 px-3 py-3">
|
||||
<details className="border border-dashed border-border/70 bg-background px-3 py-3">
|
||||
<summary className="cursor-pointer text-sm font-medium">Advanced runtime JSON</summary>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Override the inherited workspace command model only when this execution workspace truly needs different service or job behavior.
|
||||
@@ -746,7 +747,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
<label htmlFor="inherit-runtime-config">Inherit project workspace runtime config</label>
|
||||
</div>
|
||||
<textarea
|
||||
className="min-h-64 w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60 sm:min-h-96"
|
||||
className="min-h-64 w-full rounded-none border border-border bg-background px-3 py-2 font-mono text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60 sm:min-h-96"
|
||||
value={form.workspaceRuntime}
|
||||
onChange={(event) => setForm((current) => current ? { ...current, workspaceRuntime: event.target.value } : current)}
|
||||
disabled={form.inheritRuntime}
|
||||
@@ -758,13 +759,13 @@ export function ExecutionWorkspaceDetail() {
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<Button className="w-full sm:w-auto" disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}>
|
||||
<Button className="w-full rounded-none sm:w-auto" disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}>
|
||||
{updateWorkspace.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Save changes
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
className="w-full rounded-none sm:w-auto"
|
||||
disabled={!isDirty || updateWorkspace.isPending}
|
||||
onClick={() => {
|
||||
setForm(initialState);
|
||||
@@ -780,7 +781,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-border bg-card p-4 sm:p-5">
|
||||
<div className="border border-border bg-background p-4 sm:p-5">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Linked objects</div>
|
||||
<h2 className="text-lg font-semibold">Workspace context</h2>
|
||||
@@ -825,7 +826,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
</DetailRow>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-border bg-card p-4 sm:p-5">
|
||||
<div className="border border-border bg-background p-4 sm:p-5">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Paths and refs</div>
|
||||
<h2 className="text-lg font-semibold">Concrete location</h2>
|
||||
@@ -870,7 +871,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
</div>
|
||||
</div>
|
||||
) : activeTab === "runtime_logs" ? (
|
||||
<div className="rounded-2xl border border-border bg-card p-4 sm:p-5">
|
||||
<div className="border border-border bg-background p-4 sm:p-5">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Recent operations</div>
|
||||
<h2 className="text-lg font-semibold">Runtime and cleanup logs</h2>
|
||||
@@ -887,7 +888,7 @@ export function ExecutionWorkspaceDetail() {
|
||||
) : workspaceOperationsQuery.data && workspaceOperationsQuery.data.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{workspaceOperationsQuery.data.map((operation) => (
|
||||
<div key={operation.id} className="rounded-xl border border-border/80 bg-background px-3 py-2">
|
||||
<div key={operation.id} className="border border-border/80 bg-background px-3 py-2">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">{operation.command ?? operation.phase}</div>
|
||||
|
||||
Reference in new issue
Block a user