mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
Simplify agent harness configuration choices (#17722)
## Thinking Path
> - Paperclip manages AI agents and their work.
> - Users choose a harness in agent setup and configuration.
> - Supported harnesses already determine their native Runner profile.
> - A second ACP agent selector lets users make a conflicting choice.
> - The harness list also shows unavailable options marked Coming soon.
> - This PR removes these extra choices and keeps each harness's
settings.
## Linked Issues or Issue Description
Refs #17142 and #17120. Related runner defaults work: #15422.
**What existing behavior does this improve?**
The Harness dropdown and native agent configuration form.
**Current behavior**
A Pi or Claude agent shows an ACP agent selector below Harness. Users
can select a different agent in that second control. The Harness list
also includes disabled Coming soon entries.
**Proposed behavior**
Use Harness as the only agent choice when it determines the Runner
profile. Hide unavailable entries from its list. Keep the generic
advanced Runner selector available where no parent harness determines
the profile.
**Reason and benefit**
Users choose the harness once. The form no longer offers a conflicting
ACP agent choice or an option they cannot use.
**Breaking changes**
No API or stored configuration changes. Unavailable harnesses are
omitted from the list; existing saved values still display.
## What Changed
- Hide the ACP agent selector when the parent Harness control determines
the Runner profile.
- Remove Coming soon options and their unused disabled-state rendering
from the Harness dropdown.
- Preserve saved Process and HTTP selections during unrelated edits
while omitting them from new choices.
- Document the field visibility contract.
- Cover new and saved Claude, Pi, Codex, and OpenCode forms. Check that
Pi thinking controls remain and an unrelated name edit does not rewrite
Runner configuration.
## Verification
- Passed after rebasing onto master: 199 tests across
AgentConfigForm.render, codex-local/config-fields, and adapter config
sections.
- Passed: UI TypeScript check, UI production build, token gates, and git
diff --check.
- Live browser verification passed for the Pi configuration page before
and after rebase. Harness remains Pi, ACP agent is absent, and the
original model and thinking level remain. The open Harness list contains
no Coming soon, Process, or HTTP entries.
- Dropdown regression cases open Harness, check the supported options,
exclude Coming soon/Process/HTTP options, and preserve saved Process and
HTTP configuration through a name-only save.
- Passed on final commit b60a02387f: all 57 check runs are terminal and
clean (53 success, 2 neutral, 2 skipped). The Snyk status is successful.
Greptile and Paperclip review both give 5/5, with no unresolved review
threads.
## Risks
- Low risk: this change only removes redundant or unavailable UI
choices. Runner routing and saved settings keep their existing
contracts.
- The advanced generic Runner form retains ACP profile selection. It has
no parent harness choice to determine that profile.
- No new agent task or provider call was needed to verify these form
changes.
## Model Used
OpenAI GPT-6 through Codex, with reasoning, code editing, command
execution, and browser use. The exact serving model ID and
context-window size are 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
41c42895fa
commit
5d2515e520
4 files changed
+49
-4
No files matched your search
@@ -309,7 +309,7 @@ export function CodexLocalConfigFields({
|
||||
<ToggleField label="Allow externally billed provider" hint="Muse does not report usage or cost. Paperclip cannot enforce a Muse spend ceiling; known company and agent budget limits still apply."
|
||||
checked={runnerSchemaValue("allowUnmeteredProvider", false) === true} onChange={value => updateRunnerSchemaValue("allowUnmeteredProvider", value)} />
|
||||
</>}
|
||||
{runnerManaged && runnerProvider === "acpx" && runnerSchemaValue("acpxAgent", "claude") !== "grok" && (
|
||||
{runnerManaged && !hideRunnerHarness && runnerProvider === "acpx" && runnerSchemaValue("acpxAgent", "claude") !== "grok" && (
|
||||
<Field configSection="adapter" label="ACP agent" hint="Cursor uses Paperclip questions; per-run cost is unavailable. Pi accepts any provider/model ID. GitHub Copilot awaits qualification.">
|
||||
<Select
|
||||
value={String(isCreate ? values!.adapterSchemaValues?.acpxAgent ?? "claude" : eff("adapterConfig", "acpxAgent", config.acpxAgent ?? "claude"))}
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface AdapterConfigFieldsProps {
|
||||
openAiDotEnabled?: boolean;
|
||||
/** Personal Muse is offered only with its own flag and the native Runner enabled. */
|
||||
museEnabled?: boolean;
|
||||
/** The parent Harness control determines the profile; hide runner and ACP agent selectors. */
|
||||
hideRunnerHarness?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -719,6 +719,51 @@ describe("AgentConfigForm environment selector", () => {
|
||||
expect(result.onSave).toHaveBeenCalledWith({ name: "Renamed Cody" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ harness: "claude_local", label: "Claude", profile: { provider: "acpx", acpxAgent: "claude" } },
|
||||
{ harness: "pi_local", label: "Pi", profile: { provider: "acpx", acpxAgent: "pi" } },
|
||||
{ harness: "codex_local", label: "Codex", profile: { provider: "codex" } },
|
||||
{ harness: "opencode_local", label: "OpenCode", profile: { provider: "opencode" } },
|
||||
])("uses only Harness to choose $label for new and saved native agents", async ({ harness, label, profile }) => {
|
||||
mockAdaptersApi.list.mockResolvedValue([{ type: harness, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" }]);
|
||||
const created = await renderCreateForm([], { adapterType: harness, runner: "auto" });
|
||||
roots.push(created.root);
|
||||
expect(created.container.querySelector('[aria-label="ACP agent"]')).toBeNull();
|
||||
if (harness === "pi_local") expect(created.container.querySelector('[aria-label="Pi thinking level"]')).not.toBeNull();
|
||||
|
||||
const edited = await renderForm([], { adapterType: "paperclip_runner", adapterConfig: profile }, { showAdapterTypeField: true });
|
||||
roots.push(edited.root);
|
||||
expect(edited.container.querySelector('[aria-label="Harness"]')?.textContent).toContain(label);
|
||||
expect(edited.container.querySelector('[aria-label="ACP agent"]')).toBeNull();
|
||||
if (harness === "pi_local") expect(edited.container.querySelector('[aria-label="Pi thinking level"]')).not.toBeNull();
|
||||
await act(async () => setInputValue(edited.container.querySelector<HTMLInputElement>('[placeholder="Agent name"]')!, "Renamed agent"));
|
||||
await flushReact();
|
||||
await clickByText(edited.container, "Save");
|
||||
expect(edited.onSave).toHaveBeenCalledWith({ name: "Renamed agent" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ adapterType: "process", label: "Process", adapterConfig: { command: "node", args: ["worker.js"] } },
|
||||
{ adapterType: "http", label: "HTTP", adapterConfig: { url: "https://example.com/agent" } },
|
||||
])("omits coming-soon options but preserves a saved $label agent", async ({ adapterType, label, adapterConfig }) => {
|
||||
const result = await renderForm([], { adapterType, adapterConfig }, { showAdapterTypeField: true });
|
||||
roots.push(result.root);
|
||||
expect(result.container.querySelector('[aria-label="Harness"]')?.textContent).toContain(label);
|
||||
await clickElement(result.container.querySelector('[aria-label="Harness"]'));
|
||||
const options = document.querySelector('[role="listbox"][aria-label="Harness"]');
|
||||
expect(options).not.toBeNull();
|
||||
const values = Array.from(options!.querySelectorAll('[role="option"]'), option => option.getAttribute("data-value"));
|
||||
expect(values).toEqual(expect.arrayContaining(["claude_local", "codex_local", "opencode_local", "pi_local"]));
|
||||
expect(values).not.toContain("process");
|
||||
expect(values).not.toContain("http");
|
||||
expect(options!.textContent).not.toContain("Coming soon");
|
||||
await clickElement(result.container.querySelector('[aria-label="Harness"]'));
|
||||
await act(async () => setInputValue(result.container.querySelector<HTMLInputElement>('[placeholder="Agent name"]')!, "Renamed saved agent"));
|
||||
await flushReact();
|
||||
await clickByText(result.container, "Save");
|
||||
expect(result.onSave).toHaveBeenCalledWith({ name: "Renamed saved agent" });
|
||||
});
|
||||
|
||||
it.each(["codex_local", "claude_local"])("upgrades a saved %s in one click with pending edits", async adapterType => {
|
||||
mockAdaptersApi.list.mockResolvedValueOnce([{ type: adapterType, supportedRunners: ["legacy", "paperclip"], defaultRunner: "paperclip" }]);
|
||||
const result = await renderForm([], { adapterType, adapterConfig: { model: "existing-model", effort: "high" } });
|
||||
|
||||
@@ -3877,7 +3877,7 @@ export function AdapterTypeDropdown({
|
||||
[...listAdapterOptions(type => adapterLabels[type] ?? getAdapterLabel(type)),
|
||||
...(openAiDotEnabled ? [{ value: "openai_dot", label: "OpenAI Dot", experimental: true, comingSoon: false }] : []),
|
||||
...(museEnabled ? [{ value: "muse", label: "Muse — Personal agent", experimental: true, comingSoon: false }] : []),
|
||||
].filter(item => (item.value !== "paperclip_runner" || nativeRunnerEnabled) && !disabledTypes.has(item.value))
|
||||
].filter(item => !item.comingSoon && (item.value !== "paperclip_runner" || nativeRunnerEnabled) && !disabledTypes.has(item.value))
|
||||
.sort((a, b) => Number(a.value === "paperclip_runner") - Number(b.value === "paperclip_runner")),
|
||||
[disabledTypes, openAiDotEnabled, nativeRunnerEnabled, museEnabled]);
|
||||
return <SelectPopover aria-label="Harness" value={value} onValueChange={onChange}
|
||||
@@ -3886,10 +3886,9 @@ export function AdapterTypeDropdown({
|
||||
<span className="truncate">{adapterLabels[value] ?? getAdapterLabel(value)}</span>
|
||||
{selectedDisplay.experimental && <ExperimentalBadge />}
|
||||
</span>}
|
||||
options={adapterList.map(item => ({ value: item.value, disabled: item.comingSoon,
|
||||
options={adapterList.map(item => ({ value: item.value,
|
||||
group: item.value === "paperclip_runner" ? "Advanced" : undefined,
|
||||
label: <span className="inline-flex items-center gap-1.5"><span aria-hidden="true" className="inline-flex shrink-0"><AdapterMark type={item.value} className="size-4" /></span><span>{item.label}</span>{item.experimental && <ExperimentalBadge />}</span>,
|
||||
suffix: item.comingSoon ? <span className="text-(length:--text-nano) text-muted-foreground">Coming soon</span> : undefined,
|
||||
}))} />;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user