Polish board settings and skills workflow (#4863)

## Thinking Path

> - Paperclip's board UI and bundled skills are the operator layer for
configuring agents, routines, issue workflows, and local troubleshooting
loops.
> - The prior rollup mixed this operator polish with database backups,
backend reliability, thread scale, and cost/workflow primitives.
> - This pull request isolates the remaining board QoL, settings,
issue-detail integration, adapter config cleanup, and skills smoke
tooling.
> - It includes some integration-level overlap with the thread and
workflow slices so this branch can run from `origin/master` while still
preserving the full original work.
> - Preferred merge order is the narrower primitives first, then this
integration PR last.
> - The benefit is that reviewers can inspect the user-facing
board/settings/skills layer separately from backend infrastructure
changes.

## What Changed

- Added board/settings polish for agents, routines, company settings,
project workspace detail, and issue detail controls.
- Added agent/routine UI regression tests and New Issue dialog coverage.
- Integrated issue-detail activity/cost/interaction surfaces and leaf
work pause/resume controls.
- Cleaned bundled adapter UI config defaults and onboarding copy.
- Added terminal-bench loop and work-stoppage diagnosis skills plus a
smoke test script.
- Updated attachment type handling and Paperclip skill/API guidance.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/pages/Agents.test.tsx
ui/src/pages/Routines.test.tsx ui/src/components/NewIssueDialog.test.tsx
ui/src/pages/IssueDetail.test.tsx
server/src/__tests__/costs-service.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/issue-thread-interactions-service.test.ts`
- Result: 7 test files passed, 54 tests passed.
- `pnpm run smoke:terminal-bench-loop-skill`
- Result: JSON output included `"ok": true` and `"cleanup": true`.
- UI screenshots not included because verification is focused
component/page coverage for the changed board surfaces.

## Risks

- This is the integration-heavy PR in the split and intentionally
overlaps some component/API primitives with the issue-thread and
workflow PRs so it can run from `origin/master`.
- Preferred merge order: #4859, #4860, #4861, #4862, then this PR last.
If earlier branches merge first, this PR may need a straightforward
conflict refresh in shared UI files.
- The terminal-bench smoke script creates temporary mock issues and
relies on cleanup; the verified run returned `cleanup: true`.

> 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, GPT-5.5, code execution and GitHub CLI tool use, medium
reasoning effort.

## 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
- [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-30 15:28:11 -05:00 committed by GitHub
parent c4269bab59
commit 1fe1067361
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1718 additions and 173 deletions

View file

@ -784,28 +784,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
</Field>
)}
{/* Prompt template (create mode only — edit mode shows this in Identity) */}
{isLocal && isCreate && (
<>
<Field label="Prompt Template" hint={help.promptTemplate}>
<MarkdownEditor
value={val!.promptTemplate}
onChange={(v) => set!({ promptTemplate: v })}
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
contentClassName="min-h-[88px] text-sm font-mono"
imageUploadHandler={async (file) => {
const namespace = "agents/drafts/prompt-template";
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
return asset.contentPath;
}}
/>
</Field>
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
Prompt template is replayed on every heartbeat. Prefer small task framing and variables like <code>{"{{ context.* }}"}</code> or <code>{"{{ run.* }}"}</code>; avoid repeating stable instructions here.
</div>
</>
)}
{/* Adapter-specific fields are rendered inside Permissions & Configuration */}
</div>

View file

@ -122,6 +122,9 @@ interface IssueChatMessageContext {
options?: { allowSharing?: boolean; reason?: string },
) => Promise<void>;
onStopRun?: (runId: string) => Promise<void>;
stopRunLabel?: string;
stoppingRunLabel?: string;
stopRunVariant?: "stop" | "pause";
onInterruptQueued?: (runId: string) => Promise<void>;
onCancelQueued?: (commentId: string) => void;
onImageClick?: (src: string) => void;
@ -137,6 +140,9 @@ interface IssueChatMessageContext {
interaction: AskUserQuestionsInteraction,
answers: AskUserQuestionsAnswer[],
) => Promise<void> | void;
onCancelInteraction?: (
interaction: AskUserQuestionsInteraction,
) => Promise<void> | void;
}
const IssueChatCtx = createContext<IssueChatMessageContext>({
@ -273,6 +279,9 @@ interface IssueChatThreadProps {
onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise<void>;
onCancelRun?: () => Promise<void>;
onStopRun?: (runId: string) => Promise<void>;
stopRunLabel?: string;
stoppingRunLabel?: string;
stopRunVariant?: "stop" | "pause";
imageUploadHandler?: (file: File) => Promise<string>;
onAttachImage?: (file: File) => Promise<IssueAttachment | void>;
draftKey?: string;
@ -308,6 +317,9 @@ interface IssueChatThreadProps {
interaction: AskUserQuestionsInteraction,
answers: AskUserQuestionsAnswer[],
) => Promise<void> | void;
onCancelInteraction?: (
interaction: AskUserQuestionsInteraction,
) => Promise<void> | void;
composerRef?: Ref<IssueChatComposerHandle>;
/**
* Hook for the parent to refetch comments when the user explicitly asks
@ -1335,6 +1347,9 @@ function IssueChatAssistantMessage({
onVote,
agentMap,
onStopRun,
stopRunLabel = "Stop run",
stoppingRunLabel = "Stopping...",
stopRunVariant = "stop",
} = useContext(IssueChatCtx);
const custom = message.metadata.custom as Record<string, unknown>;
const anchorId = typeof custom.anchorId === "string" ? custom.anchorId : undefined;
@ -1528,13 +1543,21 @@ function IssueChatAssistantMessage({
{canStopRun && onStopRun && runId ? (
<DropdownMenuItem
disabled={isStoppingRun}
className="text-red-700 focus:text-red-800 dark:text-red-300 dark:focus:text-red-200"
className={cn(
stopRunVariant === "pause"
? "text-amber-700 focus:text-amber-800 dark:text-amber-300 dark:focus:text-amber-200"
: "text-red-700 focus:text-red-800 dark:text-red-300 dark:focus:text-red-200",
)}
onSelect={() => {
void onStopRun(runId);
}}
>
<Square className="mr-2 h-3.5 w-3.5 fill-current" />
{isStoppingRun ? "Stopping..." : "Stop run"}
{stopRunVariant === "pause" ? (
<PauseCircle className="mr-2 h-3.5 w-3.5" />
) : (
<Square className="mr-2 h-3.5 w-3.5 fill-current" />
)}
{isStoppingRun ? stoppingRunLabel : stopRunLabel}
</DropdownMenuItem>
) : null}
{runHref ? (
@ -1791,6 +1814,7 @@ function ExpiredRequestConfirmationActivity({
userLabelMap,
onAcceptInteraction,
onRejectInteraction,
onCancelInteraction,
} = useContext(IssueChatCtx);
const [expanded, setExpanded] = useState(false);
const hasResolvedActor = Boolean(interaction.resolvedByAgentId || interaction.resolvedByUserId);
@ -1869,6 +1893,7 @@ function ExpiredRequestConfirmationActivity({
userLabelMap={userLabelMap}
onAcceptInteraction={onAcceptInteraction}
onRejectInteraction={onRejectInteraction}
onCancelInteraction={onCancelInteraction}
/>
</div>
) : null}
@ -1884,6 +1909,7 @@ function IssueChatSystemMessage({ message }: { message: ThreadMessage }) {
onAcceptInteraction,
onRejectInteraction,
onSubmitInteractionAnswers,
onCancelInteraction,
} = useContext(IssueChatCtx);
const custom = message.metadata.custom as Record<string, unknown>;
const anchorId = typeof custom.anchorId === "string" ? custom.anchorId : undefined;
@ -1929,6 +1955,7 @@ function IssueChatSystemMessage({ message }: { message: ThreadMessage }) {
onAcceptInteraction={onAcceptInteraction}
onRejectInteraction={onRejectInteraction}
onSubmitInteractionAnswers={onSubmitInteractionAnswers}
onCancelInteraction={onCancelInteraction}
/>
</div>
</div>
@ -3061,6 +3088,9 @@ export function IssueChatThread({
onAdd,
onCancelRun,
onStopRun,
stopRunLabel,
stoppingRunLabel,
stopRunVariant,
imageUploadHandler,
onAttachImage,
draftKey,
@ -3087,6 +3117,7 @@ export function IssueChatThread({
onAcceptInteraction,
onRejectInteraction,
onSubmitInteractionAnswers,
onCancelInteraction,
composerRef,
onRefreshLatestComments,
}: IssueChatThreadProps) {
@ -3544,6 +3575,7 @@ export function IssueChatThread({
const stableOnAcceptInteraction = useStableEvent(onAcceptInteraction);
const stableOnRejectInteraction = useStableEvent(onRejectInteraction);
const stableOnSubmitInteractionAnswers = useStableEvent(onSubmitInteractionAnswers);
const stableOnCancelInteraction = useStableEvent(onCancelInteraction);
const chatCtx = useMemo<IssueChatMessageContext>(
() => ({
@ -3555,12 +3587,16 @@ export function IssueChatThread({
userProfileMap,
onVote: stableOnVote,
onStopRun: stableOnStopRun,
stopRunLabel,
stoppingRunLabel,
stopRunVariant,
onInterruptQueued: stableOnInterruptQueued,
onCancelQueued: stableOnCancelQueued,
onImageClick: stableOnImageClick,
onAcceptInteraction: stableOnAcceptInteraction,
onRejectInteraction: stableOnRejectInteraction,
onSubmitInteractionAnswers: stableOnSubmitInteractionAnswers,
onCancelInteraction: stableOnCancelInteraction,
}),
[
feedbackDataSharingPreference,
@ -3571,12 +3607,16 @@ export function IssueChatThread({
userProfileMap,
stableOnVote,
stableOnStopRun,
stopRunLabel,
stoppingRunLabel,
stopRunVariant,
stableOnInterruptQueued,
stableOnCancelQueued,
stableOnImageClick,
stableOnAcceptInteraction,
stableOnRejectInteraction,
stableOnSubmitInteractionAnswers,
stableOnCancelInteraction,
],
);