From 1c3abf5075306129fd08263d47483466430e7f76 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:39:18 -0500 Subject: [PATCH] fix(ui): use available space for composer labels (#15050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip helps people manage AI agents and their work. > - The task composer shows the next assignee and model before a message starts work. > - Fixed width limits shortened both labels when the composer had unused space. > - People could not read the selected agent and model even when the full text could fit. > - This pull request makes the capsule use the available composer width. > - It keeps the ellipsis when Plan mode or a narrow layout causes real space pressure. > - The benefit is clearer run settings without damage to the compact composer layout. ## Linked Issues or Issue Description **What happened?** The task composer truncated the assignee name at 6rem and the complete assignee and model capsule at 16rem. It did this even when the composer had more available space. **Expected behavior** The composer must show the complete assignee and model labels when they fit. It must use an ellipsis only when another control or a narrow viewport limits the available width. **Steps to reproduce** 1. Open a task composer with a long assignee name and a long model name. 2. Use a wide desktop layout. 3. Observe that the old capsule shortened both labels while unused space remained. **Paperclip version or commit** Current `master` before this change. **Deployment mode** Local dev (`pnpm dev`). **Installation method** Built from source. **Agent adapter(s) involved** Not adapter-specific. This is a core UI bug. **Database mode** Not database-related. **Access context** Board. **Additional context** The Storybook cases cover a wide composer and a narrow composer with Plan mode. ## What Changed - Removed the fixed maximum width from the assignee and model capsule. - Removed the fixed maximum width from the assignee label. - Kept overflow ellipsis behavior when the parent row has insufficient space. - Added stable label selectors and focused component coverage. - Added Storybook cases for complete labels and Plan-mode truncation. ## Verification - `pnpm --filter @paperclipai/plugin-sdk build` - `pnpm --filter @paperclipai/ui typecheck` - `vitest run ui/src/components/task-chat/ComposerRunSettingsPicker.test.tsx` - `node scripts/check-token-gates.mjs` - Storybook production build under Node.js 24.20.0 - Captured and inspected the two new Storybook cases. - The complete repository typecheck and build reach the Rust Runner step. This local environment does not have `cargo`. Hosted CI supplies the Rust toolchain. - The repository test suite reaches workspace-runtime tests. This local runner does not allow their required temporary home directories. Hosted CI supplies a writable test home. ## Risks - Low risk. The change only removes fixed width limits from one flex item. - A very narrow composer can still shorten both labels. This is the intended fallback. - The Storybook constrained case verifies that Plan mode and Send remain usable. > 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 GPT-5 through the Paperclip Codex runner. The run used reasoning, tool use, code execution, browser automation, and image inspection. ## 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) - [ ] 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 --- .../ComposerRunSettingsPicker.test.tsx | 15 ++++++++++++ .../task-chat/ComposerRunSettingsPicker.tsx | 6 ++--- .../composer-model-picker/fixtures.ts | 3 +++ .../stories/composer-model-picker.stories.tsx | 24 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/ui/src/components/task-chat/ComposerRunSettingsPicker.test.tsx b/ui/src/components/task-chat/ComposerRunSettingsPicker.test.tsx index e09a165b4c..3185f1d905 100644 --- a/ui/src/components/task-chat/ComposerRunSettingsPicker.test.tsx +++ b/ui/src/components/task-chat/ComposerRunSettingsPicker.test.tsx @@ -55,6 +55,21 @@ afterEach(() => { }); describe("composer assignee picker", () => { + it("lets the assignee and model use the available composer width", () => { + render(vi.fn(), vi.fn()); + const trigger = container!.querySelector('[data-testid="task-chat-composer-assignee"]'); + const assignee = trigger!.querySelector('[data-testid="task-chat-composer-assignee-label"]'); + const model = trigger!.querySelector('[data-testid="task-chat-composer-model-label"]'); + + expect(trigger?.className).toContain("max-w-full"); + expect(trigger?.className).not.toContain("max-w-64"); + expect(assignee?.className).toContain("min-w-0"); + expect(assignee?.className).not.toContain("max-w-24"); + expect(model?.className).toContain("min-w-0"); + expect(assignee?.className).toContain("truncate"); + expect(model?.className).toContain("truncate"); + }); + it("finds assignees by their displayed role and harness", async () => { render(vi.fn(), vi.fn()); await click("Select assignee, model and effort"); diff --git a/ui/src/components/task-chat/ComposerRunSettingsPicker.tsx b/ui/src/components/task-chat/ComposerRunSettingsPicker.tsx index f3b6025126..63f42235d2 100644 --- a/ui/src/components/task-chat/ComposerRunSettingsPicker.tsx +++ b/ui/src/components/task-chat/ComposerRunSettingsPicker.tsx @@ -167,10 +167,10 @@ export function ComposerRunSettingsPicker({ const reset = () => onSettingsChange(DEFAULT_COMPOSER_RUN_SETTINGS); const closeButton = mobile ? : null; const trigger = ; diff --git a/ui/storybook/prototypes/composer-model-picker/fixtures.ts b/ui/storybook/prototypes/composer-model-picker/fixtures.ts index c194f2b773..2ea83b9771 100644 --- a/ui/storybook/prototypes/composer-model-picker/fixtures.ts +++ b/ui/storybook/prototypes/composer-model-picker/fixtures.ts @@ -86,6 +86,9 @@ export const composerAgents: ComposerAgent[] = [ { id: "kimi-code/kimi-for-coding", label: "K2.8 Preview" }, { id: "kimi-code/k3", label: "K3" }, ], manualPattern: "kimi-code/model" }, + { id: "long-labels", name: "Alexandra Engineering Coordinator", role: "Engineering", harness: "Codex", adapterType: "codex_local", defaultModel: "gpt-5.6-sol", models: [ + { id: "gpt-5.6-sol", label: "GPT-5.6 Sol Extended Context Preview", detail: "Agent default" }, + ], manualPattern: "Model ID, e.g. gpt-5.6-sol" }, ]; /** Share the capsule-avatar palettes used by the agent persona stories. */ diff --git a/ui/storybook/stories/composer-model-picker.stories.tsx b/ui/storybook/stories/composer-model-picker.stories.tsx index dbe7cf8c05..391eddb75c 100644 --- a/ui/storybook/stories/composer-model-picker.stories.tsx +++ b/ui/storybook/stories/composer-model-picker.stories.tsx @@ -414,3 +414,27 @@ export const ProductionIntermediateWidthPlan: Story = { await expect(screen.getByTestId("composer-model-popover")).toBeVisible(); }, }; + +export const ProductionLongLabelsWide: Story = { + name: "25 · Long labels with available space", + render: () => , + play: async ({ canvasElement }) => { + const screen = within(canvasElement.ownerDocument.body); + const assignee = screen.getByTestId("task-chat-composer-assignee-label"); + const model = screen.getByTestId("task-chat-composer-model-label"); + await expect(assignee.scrollWidth).toBeLessThanOrEqual(assignee.clientWidth); + await expect(model.scrollWidth).toBeLessThanOrEqual(model.clientWidth); + }, +}; + +export const ProductionLongLabelsConstrained: Story = { + name: "25b · Long labels constrained by Plan mode", + render: () => , + play: async ({ canvasElement }) => { + const screen = within(canvasElement.ownerDocument.body); + const assignee = screen.getByTestId("task-chat-composer-assignee-label"); + const model = screen.getByTestId("task-chat-composer-model-label"); + await expect(assignee.scrollWidth > assignee.clientWidth || model.scrollWidth > model.clientWidth).toBe(true); + await expect(screen.getByRole("button", { name: "Remove Plan mode" })).toBeVisible(); + }, +};