fix(ui): wrap company skill source paths (#9405)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company skills expose their source metadata in a narrow details
sidebar
> - Long filesystem paths and repository locators were truncated, hiding
the part operators often need to distinguish sources
> - The sidebar can preserve the complete value by wrapping at arbitrary
path boundaries instead of ellipsizing it
> - This pull request renders full source paths and repository labels
without widening the layout
> - The benefit is that operators can inspect and copy the actual skill
source from the UI

## Linked Issues or Issue Description

### Pre-submission checklist

- [x] I have searched existing open and closed issues and this is not a
duplicate.
- [x] I can reproduce this on `master`.
- [x] I have confirmed the behavior originates in Paperclip itself, not
an agent adapter, API provider, or local configuration.

### What happened?

Long company-skill source paths and repository locators were truncated
in the skill details sidebar.

### Expected behavior

The complete source value remains visible and wraps within the available
sidebar width.

### Steps to reproduce

1. Open a company skill whose source path is longer than the details
sidebar.
2. View the Source field.
3. Observe that the old UI replaces the middle or end of the value with
an ellipsis.

### Paperclip version or commit

`origin/master` at `02e2dd271`.

### Deployment mode

Local dev (`pnpm dev`).

### Installation method

Built from source (`pnpm dev` / `pnpm build`).

### Agent adapter(s) involved

None; this is a company-skills UI layout issue.

### Logs, configuration, or screenshots

Not applicable; the behavior is directly visible in the Source field.

### Additional context

The narrow sidebar should remain width-constrained. Wrapping
intentionally trades vertical space for full source inspectability.

## What Changed

- Replace source-path truncation with width-constrained arbitrary
wrapping.
- Apply the same wrapping behavior to linked repository/source labels.
- Add a regression test proving the full long path is rendered without
ellipsis.

## Verification

- `vitest --project @paperclipai/ui src/pages/CompanySkills.test.tsx` —
11 tests passed.
- `node scripts/check-token-gates.mjs` — all token gates clean.

## Risks

- Low risk: the change is limited to text layout in the company skill
details view.
- Very long unbroken values may make the Source section taller,
intentionally trading vertical space for inspectability.

> 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.3 Codex, reasoning with repository tool use and
code execution; context-window size was not exposed by the runtime.

## 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
- [ ] 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:
Dotta 2026-07-11 03:02:11 -05:00 committed by GitHub
parent 02e2dd271b
commit 5618ea91f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 30 additions and 11 deletions

View File

@ -390,6 +390,26 @@ describe("SkillDetailPage settings", () => {
expect(onFork).toHaveBeenCalledOnce();
});
it("renders long source paths in full so they can wrap inside the sidebar", async () => {
const v1 = makeVersion(1, "# Demo Skill");
const longSourcePath = "/srv/paperclip/home/paperclipai/paperclip/.agents/skills/prepare-pr/SKILL.md";
const node = await renderSkillDetail([v1], {
activeTab: "agents",
detail: makeDetail(v1, {
sourcePath: longSourcePath,
sourceLocator: null,
}),
});
const sourceValue = Array.from(node.querySelectorAll("div")).find((element) =>
element.textContent === longSourcePath,
);
expect(sourceValue).toBeTruthy();
expect(sourceValue?.className).toContain("[overflow-wrap:anywhere]");
expect(node.textContent).not.toContain("...");
});
it("saves normalized category edits from the settings dialog", async () => {
const v1 = makeVersion(1, "# Demo Skill");
const onUpdateSettings = vi.fn();

View File

@ -2588,10 +2588,9 @@ export function SkillDetailPage({
? githubSource.url
?? `https://${githubSource.hostname}/${githubSource.owner}/${githubSource.repo}/tree/${githubSource.ref}/${githubSource.path}`.replace(/\/$/, "")
: null;
// Fallback for non-catalog skills: the recorded locator/path, middle-truncated
// so long file paths stay readable in the narrow sidebar.
// Fallback for non-catalog skills: the recorded locator/path wraps inside
// the narrow sidebar instead of widening the page.
const sourceLocatorText = skill.sourcePath || skill.sourceLocator || null;
const sourceLocatorDisplay = sourceLocatorText ? middleTruncate(sourceLocatorText, 44) : null;
const sourceHref =
skill.homepageUrl
?? (sourceLocatorText && /^(https?:\/\/|[\w.-]+\.[a-z]{2,}\/)/i.test(sourceLocatorText)
@ -2709,7 +2708,7 @@ export function SkillDetailPage({
</div>
<div className="min-w-0 border-b border-border py-2">
<div className="text-xs text-muted-foreground">Source</div>
<div className="mt-1 truncate">{skill.sourcePath ?? source.label}</div>
<div className="mt-1 min-w-0 [overflow-wrap:anywhere]">{sourceLocatorText ?? source.label}</div>
</div>
<div className="min-w-0 border-b border-border py-2">
<div className="text-xs text-muted-foreground">Version</div>
@ -3054,9 +3053,9 @@ export function SkillDetailPage({
target="_blank"
rel="noreferrer"
title={githubRepoText ?? undefined}
className="mt-0.5 flex max-w-full items-center gap-1 text-xs text-muted-foreground no-underline transition-colors hover:text-foreground"
className="mt-0.5 flex max-w-full items-start gap-1 text-xs text-muted-foreground no-underline transition-colors [overflow-wrap:anywhere] hover:text-foreground"
>
<span className="truncate">{githubRepoText}</span>
<span className="min-w-0 [overflow-wrap:anywhere]">{githubRepoText}</span>
<ExternalLink className="h-3 w-3 shrink-0" aria-hidden="true" />
</a>
<div className="mt-0.5 truncate font-mono text-(length:--text-micro) text-muted-foreground" title={githubSource.commit}>
@ -3070,21 +3069,21 @@ export function SkillDetailPage({
<SourceIcon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
<div className="min-w-0">
<div className="text-foreground">{source.label}</div>
{sourceLocatorDisplay ? (
{sourceLocatorText ? (
sourceHref ? (
<a
href={sourceHref}
target="_blank"
rel="noreferrer"
title={sourceLocatorText ?? undefined}
className="mt-0.5 flex max-w-full items-center gap-1 text-xs text-muted-foreground no-underline transition-colors hover:text-foreground"
className="mt-0.5 flex max-w-full items-start gap-1 text-xs text-muted-foreground no-underline transition-colors [overflow-wrap:anywhere] hover:text-foreground"
>
<span className="truncate">{sourceLocatorDisplay}</span>
<span className="min-w-0 [overflow-wrap:anywhere]">{sourceLocatorText}</span>
<ExternalLink className="h-3 w-3 shrink-0" aria-hidden="true" />
</a>
) : (
<div className="mt-0.5 truncate text-xs text-muted-foreground" title={sourceLocatorText ?? undefined}>
{sourceLocatorDisplay}
<div className="mt-0.5 min-w-0 text-xs text-muted-foreground [overflow-wrap:anywhere]" title={sourceLocatorText ?? undefined}>
{sourceLocatorText}
</div>
)
) : (