[codex] Split reusable agent hiring templates (#4124)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Hiring new agents depends on clear, reusable operating instructions
> - The create-agent skill had one large template reference that mixed
multiple roles together
> - That made it harder to reuse, review, and adapt role-specific
instructions during governed hires
> - This pull request splits the reusable agent instruction templates
into focused role files and polishes the agent instructions pane layout
> - The benefit is faster, clearer agent hiring without bloating the
main skill document

## What Changed

- Split coder, QA, and UX designer reusable instructions into dedicated
reference files.
- Kept the index reference concise and pointed it at the role-specific
files.
- Updated the create-agent skill to describe the separated template
structure.
- Polished the agent detail instructions/package file tree layout so the
longer template references remain readable.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm --filter @paperclipai/ui typecheck`
- UI screenshot rationale: no screenshots attached because the visible
change is limited to the Agent detail instructions file-tree layout
(`wrapLabels` plus the side-by-side breakpoint). There is no new user
flow or state transition to demonstrate; reviewers can verify visually
by opening an agent's Instructions tab and resizing across the
single-column and side-by-side breakpoints to confirm long file names
wrap instead of truncating or overflowing.

## Risks

- Low risk: this is documentation and UI layout only.
- Main risk is stale links in the skill references; the new files are
committed in the referenced paths.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent based on GPT-5, tool-enabled local shell and
GitHub workflow, exact runtime context window not exposed in this
session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots, or documented why targeted component/type verification is
sufficient here
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-04-20 10:33:19 -05:00 committed by GitHub
parent 73eb23734f
commit 0f4e4b4c10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 284 additions and 148 deletions

View file

@ -177,6 +177,7 @@ export function PackageFileTree({
renderFileExtra,
fileRowClassName,
showCheckboxes = true,
wrapLabels = false,
depth = 0,
}: {
nodes: FileTreeNode[];
@ -191,6 +192,8 @@ export function PackageFileTree({
/** Optional additional className for file rows */
fileRowClassName?: (node: FileTreeNode, checked: boolean) => string | undefined;
showCheckboxes?: boolean;
/** Allow long file and directory names to wrap instead of forcing horizontal overflow. */
wrapLabels?: boolean;
depth?: number;
}) {
const effectiveCheckedFiles = checkedFiles ?? new Set<string>();
@ -239,7 +242,9 @@ export function PackageFileTree({
<Folder className="h-3.5 w-3.5" />
)}
</span>
<span className="truncate">{node.name}</span>
<span className={cn("min-w-0", wrapLabels ? "break-all leading-4" : "truncate")}>
{node.name}
</span>
</button>
<button
type="button"
@ -265,6 +270,7 @@ export function PackageFileTree({
renderFileExtra={renderFileExtra}
fileRowClassName={fileRowClassName}
showCheckboxes={showCheckboxes}
wrapLabels={wrapLabels}
depth={depth + 1}
/>
)}
@ -307,7 +313,9 @@ export function PackageFileTree({
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
<FileIcon className="h-3.5 w-3.5" />
</span>
<span className="truncate">{node.name}</span>
<span className={cn("min-w-0", wrapLabels ? "break-all leading-4" : "truncate")}>
{node.name}
</span>
</button>
{renderFileExtra?.(node, checked)}
</div>

View file

@ -1037,15 +1037,8 @@ export function AgentDetail() {
)}
{/* Floating Save/Cancel (desktop) */}
{!isMobile && (
<div
className={cn(
"sticky top-6 z-10 float-right transition-opacity duration-150",
showConfigActionBar
? "opacity-100"
: "opacity-0 pointer-events-none"
)}
>
{!isMobile && showConfigActionBar && (
<div className="fixed bottom-6 right-6 z-30">
<div className="flex items-center gap-2 bg-background/90 backdrop-blur-sm border border-border rounded-lg px-3 py-1.5 shadow-lg">
<Button
variant="ghost"
@ -1707,6 +1700,7 @@ function PromptsTab({
const [pendingFiles, setPendingFiles] = useState<string[]>([]);
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
const [filePanelWidth, setFilePanelWidth] = useState(260);
const [instructionPaneWidth, setInstructionPaneWidth] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [awaitingRefresh, setAwaitingRefresh] = useState(false);
const lastFileVersionRef = useRef<string | null>(null);
@ -1854,6 +1848,26 @@ function PromptsTab({
setExpandedDirs((current) => (setsEqual(current, nextExpanded) ? current : nextExpanded));
}, [visibleFilePaths]);
useEffect(() => {
if (isMobile) {
setInstructionPaneWidth(null);
return;
}
const element = containerRef.current;
if (!element) return;
const updateWidth = () => setInstructionPaneWidth(element.getBoundingClientRect().width);
updateWidth();
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) setInstructionPaneWidth(entry.contentRect.width);
});
observer.observe(element);
return () => observer.disconnect();
}, [bundleLoading, isMobile, visibleFilePaths.length]);
useEffect(() => {
const versionKey = selectedFileExists && selectedFileDetail
? `${selectedFileDetail.path}:${selectedFileDetail.content}`
@ -1978,6 +1992,9 @@ function PromptsTab({
document.body.style.userSelect = "none";
}, [filePanelWidth]);
const instructionsSideBySide =
!isMobile && instructionPaneWidth !== null && instructionPaneWidth >= filePanelWidth + 520;
if (!isLocal) {
return (
<div className="max-w-3xl">
@ -2011,8 +2028,8 @@ function PromptsTab({
</CollapsibleTrigger>
<CollapsibleContent className="pt-4 pb-6">
<TooltipProvider>
<div className="grid gap-x-6 gap-y-4 sm:grid-cols-[auto_1fr_1fr]">
<label className="space-y-1.5">
<div className="grid gap-x-6 gap-y-4 md:grid-cols-[auto_minmax(0,1fr)_minmax(12rem,0.65fr)]">
<label className="space-y-1.5 min-w-0">
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
Mode
<Tooltip>
@ -2157,12 +2174,20 @@ function PromptsTab({
</CollapsibleContent>
</Collapsible>
<div ref={containerRef} className={cn("flex gap-0", isMobile && "flex-col gap-3")}>
<div
ref={containerRef}
className="grid min-w-0 gap-3"
style={
instructionsSideBySide
? { gridTemplateColumns: `${filePanelWidth}px 0.5rem minmax(0, 1fr)` }
: undefined
}
>
<div className={cn(
"border border-border rounded-lg p-3 space-y-3 shrink-0",
"min-w-0 w-full border border-border rounded-lg p-3 space-y-3",
isMobile && showFilePanel && "block",
isMobile && !showFilePanel && "hidden",
)} style={isMobile ? undefined : { width: filePanelWidth }}>
)}>
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium">Files</h4>
<div className="flex items-center gap-1">
@ -2257,6 +2282,7 @@ function PromptsTab({
}}
onToggleCheck={() => {}}
showCheckboxes={false}
wrapLabels
renderFileExtra={(node) => {
const file = bundle?.files.find((entry) => entry.path === node.path);
if (!file) return null;
@ -2284,14 +2310,14 @@ function PromptsTab({
</div>
{/* Draggable separator */}
{!isMobile && (
{instructionsSideBySide && (
<div
className="w-1 shrink-0 cursor-col-resize hover:bg-border active:bg-primary/50 rounded transition-colors mx-1"
className="w-1 cursor-col-resize rounded transition-colors hover:bg-border active:bg-primary/50"
onMouseDown={handleSeparatorDrag}
/>
)}
<div className={cn("border border-border rounded-lg p-4 space-y-3 min-w-0 flex-1", isMobile && showFilePanel && "hidden")}>
<div className={cn("min-w-0 w-full overflow-hidden border border-border rounded-lg p-4 space-y-3", isMobile && showFilePanel && "hidden")}>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0">
{isMobile && (
@ -2346,7 +2372,8 @@ function PromptsTab({
value={displayValue}
onChange={(value) => setDraft(value ?? "")}
placeholder="# Agent instructions"
contentClassName="min-h-[420px] text-sm font-mono"
className="min-w-0 overflow-hidden"
contentClassName="min-h-[420px] max-w-full break-words text-sm font-mono"
imageUploadHandler={async (file) => {
const namespace = `agents/${agent.id}/instructions/${selectedOrEntryFile.replaceAll("/", "-")}`;
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
@ -2357,7 +2384,7 @@ function PromptsTab({
<textarea
value={displayValue}
onChange={(event) => setDraft(event.target.value)}
className="min-h-[420px] w-full rounded-md border border-border bg-transparent px-3 py-2 font-mono text-sm outline-none"
className="min-h-[420px] w-full min-w-0 rounded-md border border-border bg-transparent px-3 py-2 font-mono text-sm outline-none"
placeholder="File contents"
/>
)}