Files
DottaandPaperclip 7498705642 fix(ui): stabilize mobile task reading and document navigation (#15228)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People read tasks and send instructions from phones as well as
desktop browsers.
> - The mobile footer let page text show through its labels, and small
text fields made Safari zoom on focus.
> - Small scroll changes made the footer switch direction, and its
changing page padding moved the conversation.
> - Task pages also showed comments before question cards and run
history arrived, so the composer and reading position moved again.
> - Document links also used native navigation, which reset the reading
position or reloaded a task through its UUID URL. Desktop tabs were
crowded in the mobile drawer.
> - This pull request keeps navigation steady, opens documents in the
mounted task, and gives mobile readers a full-height panel with a
vertical tab selector.
> - The benefit is a stable task view and smoother scrolling on mobile.

## Linked Issues or Issue Description

**What happened?**

The mobile footer was translucent. Safari zoomed when a person focused a
small text field. The footer switched abruptly while scrolling. A large
task could show saved comments, then move the page again when a question
card or run history arrived. In a local test with delayed responses, a
late question moved the mobile composer by about 374 pixels. Opening a
plan from the feed could reset the view or reload the task through a
UUID link. The mobile document drawer left part of the feed exposed
above small desktop tab controls.

**Expected behavior**

The footer has an opaque surface and moves smoothly after deliberate
scrolling. Text fields do not cause automatic focus zoom. A task shows
its initial conversation and composer together at the final scroll
position. Background refreshes keep the existing conversation visible.
Document links open in the mounted task with its existing cache and
reading position. Mobile documents fill the viewport, show a clear close
button, and offer a vertical list of open tabs.

**Steps to reproduce**

1. Open a task with many long comments and a pending question in iOS
Safari.
2. Delay its interactions, activity, and runs responses by different
amounts.
3. Reload the page and watch the conversation and composer move as each
response arrives.
4. Scroll down and back up, including small direction changes and edge
bounce.
5. Focus the task composer, search field, and new-task title and
description.
6. Open a plan or another task document from the feed, including a link
that uses the task UUID. Close the panel and check the reading position.
7. Open several documents on a phone. Switch tabs and close both active
and inactive tabs.

**Paperclip version or commit**

Developed from `1c07b5903` and rebased onto `59015846a`.

**Deployment mode**

Built from source. Tested in an isolated local test drive with iOS 26.5
Simulator Safari and Chrome. A temporary local proxy delayed independent
responses for the layout test.

Related work found in the duplicate search:

- Refs #14727. It made saved replies appear before supporting history.
This PR keeps its parallel requests and narrows the tradeoff in favor of
a stable first layout.
- Refs #14667. This open PR takes a different approach with per-run
placeholders and retries. This PR fixes the observed question/composer
movement and mobile navigation behavior.
- Refs #13095 and #13597. These earlier fixes added task scroll anchors
and skipped transcript waits for scheduled retries.
- Refs #6550. Earlier mobile board polish.
- Refs #9467. This related open PR changes list and generic tab reflow.
The task-pane selector uses a separate component.

## What Changed

- Give the mobile footer an opaque semantic surface.
- Set a base-size floor for editable text on touch devices to prevent
Safari focus zoom. Preserve larger title text.
- Share mobile scroll tracking between both layouts. Accumulate scroll
distance, ignore edge bounce and changed document bounds, and update
once per frame.
- Use shared motion tokens for the footer and composer. Keep page
padding stable and honor reduced motion.
- Wait for the initial question cards, attachments, work products,
activity, runtime selection, plan, and relevant transcript history
before the first reveal. Skip scheduled retries and older runs outside
the initial comment window.
- Bound the first reveal to 15 seconds. A stalled supporting request
leaves saved conversation and the composer accessible with an explicit
loading notice.
- Keep concealed mobile history from stretching the document. Keep the
composer mounted but concealed until the same reveal. Keep both visible
during later refreshes.
- Route first and repeated same-task document clicks in place. Recognize
UUID and identifier links. Preserve the thread history entry and feed
position. Keep modifier clicks, downloads, external links, and classic
document behavior.
- Give the mobile task panel the full viewport and safe-area padding.
Use a visible X and 44-pixel touch controls. Replace the horizontal tab
strip with a vertical selector that wraps titles and supports keyboard
focus.
- Add four interactive Storybook states for a few tabs, long names, many
tabs, and the last tab. Reuse the production selector and tab
controller.
- Add navigation and tab regressions, update first-reveal regressions,
and document the behavior in `DESIGN.md`.

## Verification

- 392 tests passed across the seven focused task-loading, scroll,
mobile-navigation, layout, and composer suites. After review fixes, all
339 tests across the four affected suites passed, including
stalled-loading fallback on mobile and desktop and the motion-token
catalog.
- All 442 focused document, tab, task-thread, and scroll tests pass. The
final click-propagation cleanup also passes all 136 task-detail tests.
UI typecheck, UI production build, and `pnpm check:token-gates` passed.
- All four cases in `artifact-tab-arrival.spec.ts` and
`text-attachment-tabs.spec.ts` pass locally, covering desktop and mobile
selection, composer focus, document rendering, and downloads of the
original bytes.
- `pnpm --filter @paperclipai/ui build-storybook` passed. Open the
mobile tab stories under `Prototypes/Task detail/Mobile tabs`.
- A local diagnostic proxy measured cached plan content at about 250 ms
after the first click. The HTML load count and task request count did
not change. Feed scroll stayed at the same position. First, repeated,
and UUID document links were tested at phone and desktop widths.
Task-reference links close their preview before the document reader
opens.
- In Chrome at desktop and phone widths, the delayed-response task
showed one complete reveal. The late question no longer moved an already
visible composer.
- In iOS Simulator Safari, verified the large-task reload, opaque
footer, navigation hide/reveal, and search/new-task/composer focus
without automatic zoom.
- Full workspace typecheck and build passed. The updated UI also passes
typecheck, production build, and token gates.
- All 54 checks pass on the final commit
`bf5c9914e61833e7cc8794a69d72cf8c7057b952` (two additional checks are
intentionally skipped). Greptile reviewed that commit at 5/5, and all
review threads are resolved.
- Full local `pnpm test:run` was attempted but stopped after
server-fixture failures. Embedded PostgreSQL startup failure reproduced
in an isolated native-interaction fixture after five startup attempts.
The broad run also reported a rapid Slack callback ordering test
failure. These server paths are unchanged by this PR, and their CI
shards pass on the latest head. The full local suite is not claimed as
passing.
- A localhost proxy stalled the activity response for 30 seconds. Chrome
revealed the available conversation after the 15-second deadline at both
desktop and phone widths, kept the composer accessible, and cleared the
loading notice when the response arrived.

## Risks

Slow initial history requests can delay the first conversation reveal by
up to 15 seconds. If that deadline expires, late data can change the
available conversation while a loading notice remains visible. The
reveal waits only for runs in the initial comment window, and later
refreshes do not conceal an existing conversation. The larger editable
text can change line wrapping on phones. Mobile navigation and composer
motion use shared tokens and respect reduced-motion settings. Mobile tab
selection changes the control layout. Document links retain URL history
while sharing the task reading position; other tasks and external links
keep their normal navigation behavior.

I checked `ROADMAP.md`. This is a fix for existing UI behavior.

## Model Used

OpenAI GPT-6 in Codex. The runtime does not expose a more specific model
ID or context-window size. The agent used reasoning, code editing,
terminal tools, and Chrome and iOS Simulator testing.

## 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>
2026-10-05 12:33:34 -05:00

14 KiB

Paperclip Design Principles

Status: v0.3 — anchor document for design-language simplification. Governs structure, not brand. Brand values (color, type, iconography) are intentionally unspecified: they are being redesigned and will land as token values only. Nothing in ui/ may hardcode them. Spacing/radius scales are likewise TBD pending the token audit (see Principle 3).

Changes from v0.2: token layer location corrected to the repo's real source (ui/src/index.css); existing token tiers inventoried; snapshot-coverage scope bounded for Run 1; the issue→task copy rename moved out of the zero-visual-change run.

What this document is for

Agents and humans modifying ui/ treat this file as the source of truth for design decisions. Storybook is the verification surface — it documents the system; it does not define it. If a change conflicts with this document, change this document first (with review) or change the code.

Product stance

Paperclip is an operational control plane: org charts, tasks, heartbeat runs, budgets, approvals, audit logs. The user is an operator scanning state and making decisions. Every screen should answer, in order: what is happening, does it need me, what do I do about it. Density in service of scanning beats whitespace in service of aesthetics — but density comes from information, never from chrome.

The token layer (where visual values live)

The single token source is ui/src/index.css (Tailwind v4; there is no tailwind config file — tokens are CSS custom properties consumed via @theme). Do NOT create a parallel token source such as ui/src/tokens/ — that would produce two sources of truth. If index.css grows unwieldy, extracted values may live in a tokens.css imported by index.css so the pipeline still has one root.

Tailwind v4 gotcha: @theme inline bakes literal values at build time. Any token that must be runtime-tunable (theme editor, dark mode overrides) must be defined in a NON-inline block.

Existing tiers already in index.css (~80+ tokens) — extraction maps to these on exact value match before minting anything new:

  1. Semantic tier — shadcn core set: --background, --foreground, --card, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, --sidebar-*, --chart-1..5 (OKLCH, light/dark overrides).
  2. Brand tier — agent gradients --agent-1a/1b..10a/10b (fixed hex) and status hues --status-task-* / --status-agent-* (WCAG-tuned; see inline comments).
  3. Domain tier — match-chip tokens --chip-match-*, annotation highlights --paperclip-doc-annotation-highlight-*, plus motion/typography tokens.

Principles

  1. One way to say each thing. One component per job. One Button, one Card, one Badge, one Table, one EmptyState. Variants are props, not new components. Before creating a component, prove no existing one covers the job.
  2. Tokens are the only source of visual values. All color, spacing, radius, type size/weight, shadow, and motion values come from the token layer. No hex, no raw px, no ad-hoc Tailwind arbitrary values (p-[13px]) in components. If a needed value doesn't exist, add a token — don't inline it. Tailwind palette classes (bg-red-500, text-zinc-400, etc.) ARE hardcoded values in spirit: they name a literal color, not a semantic role. They are in-scope debt scheduled for a dedicated future run (Run 4, cluster-by-cluster mapping to semantic tokens per doc/design/DECISION-SHEET.md B2) and are not currently gated by check-token-gates. Exception (doc/design/DECISION-SHEET.md B1 user ruling): first-party intentional one-off decoration on demo/UX-lab surfaces stays inline and allowlisted rather than minted as singleton tokens.
  3. Spacing routes through tokens; the scale comes later. During simplification, extract every spacing and radius value verbatim into tokens — do not normalize, round, or invent a scale. The final scale is a design decision made by a human after reviewing the token audit. Structural rules apply now: vertical rhythm within a container uses one gap value, not per-element margins, and siblings never carry both margin and gap.
  4. Hierarchy through structure, not decoration. Prefer position, size, and weight over borders, backgrounds, and dividers. Every border, divider, and background fill must justify itself; when in doubt, remove it. A screen should survive the removal of one visual layer.
  5. Status is systematic. States like running / paused / blocked / awaiting-approval / over-budget map to a single semantic status token set used identically everywhere (badge, row, chart, log). An operator learns the vocabulary once.
  6. Machine values look machine-made. IDs, costs, token counts, timestamps, and log output use the monospace token and consistent formatting helpers. Never format these ad hoc per screen.
  7. Words are part of the system. One name per concept across the entire UI — the canonical term is task (never issue or ticket in copy, labels, or empty states). Buttons name the action ("Approve hire," not "Submit"). Errors say what happened and what to do. Empty states say what to do first. Note: enforcing the task rename is a visible change and is explicitly OUT of the zero-visual-change extraction run; it happens in its own follow-up run.
  8. Agent-modifiable by design. The system must be changeable via instructions: single token source, lint rules that enforce it, and this document kept current. A correct change should be expressible as "edit tokens + run checks," not "visit 40 files."

Form and wizard footers

Keep Save & exit (or Cancel/Back) and the primary Continue/Connect/Finish action in one shared footer row, vertically centered. Put the subdued secondary action on the left and the primary action on the right. A step owns its whole footer: do not render Save & exit in a separate parent block below it. Check this alignment in every step and conditional state, not just the first screen.

Mobile navigation and text fields

The fixed bottom navigation uses an opaque surface so scrolling content cannot show through its labels. On touch devices, editable controls use at least the 16px base typography token to prevent Safari's automatic focus zoom. Larger title sizes remain larger.

The bottom navigation responds to accumulated scrolling, ignoring small reversals and Safari's edge bounce. It glides out and eases back in with shared motion tokens; the task composer follows the same motion. Keep page padding stable while the navigation moves, and honor reduced-motion preferences.

Task conversations reveal their initial comments, interaction cards, plan, and relevant run history together after positioning the latest message. Keep the mobile loading surface at a stable viewport height while that history loads; concealed content must not stretch the document. Background refreshes keep an already revealed conversation and composer mounted and visible. Bound the initial wait to 15 seconds. If a request stalls, reveal the available conversation and composer with a notice that some history is still loading.

Contextual feedback

Task chat shows execution errors and waits only while they remain relevant. Completing or cancelling a task hides its old execution notices. A newer attempt by the same agent or an explicit successor supersedes earlier run notices; an unresolved execution hold remains visible. Historical turns keep their responses, files, questions and inspectable activity without a Worked/Stopped status label. Run history retains the full diagnostic record. Session reset boundaries remain in the conversation. Time passing or a new human comment alone does not resolve an error. Stored notices need run or recovery provenance before they can be hidden; child-task relays and other unrelated system updates stay visible.

Do not show a toast for task or run state already visible on the current screen. This includes descendant runs represented by the open subtree. Show local action results in place; keep failures actionable inline. Notifications for other work remain useful. Repeated delivery of the same run outcome must refresh cached state without repeating its toast, including after reconnecting. A terminal outcome delivered more than five minutes after the run finished is historical and should refresh state silently. Expected cancellation is neutral gray, not an error. The composer's Stop action stops the current response and leaves the composer available for a new message. Pause work is a separate explicit task or subtree action. A paused task replaces the composer with an amber takeover. It says “Task is paused.” and “Resume this task to send a message.” with a “Resume task” action. Subtrees use “Subtree is paused.” and “Resume subtree.” The takeover cannot be dismissed, retains drafts, and hides message inputs until the pause is released.

Confirmations whose source work is still syncing show “Preparing approval…” and disable acceptance until the server reports readiness. Refresh that state automatically; rejection and revision remain available. Live tool reviews keep their own approval flow.

Pending questions, confirmations, and other task-thread inputs appear in a separate card directly above the ordinary composer. The composer stays available for new messages while the card is open. Questions use their compact history entry as the reminder; dismissing one clears the composer and stays effective after reload for that person and task. Other inputs keep a pending indicator that can reopen them; resolving or skipping the input removes that indicator.

Enforcement (what "compliant" means for the extraction run)

  • Zero visual change is proven, not promised: Storybook visual snapshots are baselined before any refactor, and all snapshots match baseline after it. A change that alters rendered output must be intentional and human-approved.
  • Baseline scope for Run 1: the shared primitives in ui/src/components/ui/ (each gets a story if missing — there are only ~24) plus the ~46 existing stories under ui/storybook/stories/. Do NOT attempt a story for every feature component (~277) in this run; full coverage is a later effort.
  • Mechanical rewrites (value extraction, renames) are done via committed codemod scripts in scripts/, not hand-edits — reviewable once, repeatable forever.
  • Token layer is the single source (ui/src/index.css, per above) consumed via CSS variables / Tailwind theme — never values copied into components.
  • Lint/grep gates pass: zero hardcoded hex values, zero arbitrary spacing values, zero raw font-size declarations in ui/src/components/** and ui/src/pages/** outside the token layer and a documented allowlist (third-party overrides, intentional opt-outs commented inline).
  • pnpm build, pnpm typecheck, and pnpm build-storybook pass.
  • AGENTS.md links here and states the token-only rule.

Aspirational (NOT gating this run): no duplicate components; every component has exactly one story covering its variants; all UI copy says "task".

Out of scope (do not do during simplification)

No visual redesign, no new colors or typefaces, no layout restructuring, no new dependencies beyond snapshot tooling, no component consolidation/merges (audit + recommend only), no copy renames, no changes to server code or app logic. Simplification means fewer parts, same product.

Prior art (read before auditing)

See doc/design/PRIOR-ART.md — a previous audit pass (PAP-280/283/284, on the PAP-282-playground branch, NOT on master) found that of ~220 hardcoded drift sites, only 6 were exact-value-mappable to existing tokens; expect the verbatim extraction to mint many new tokens that the human scale-collapse step later merges. It also drafted usage rules (radius tiers, CTA tiers, named type styles) that are good candidates for the post-audit scale decision.

How-to guide for day-to-day UI changes: see doc/design/CHANGING-THE-UI.md.

Motion tokens (Task Chat Redesign)

Mobile task panels fill the viewport within the safe area. Their top toolbar shows the current tab title, an open-tab count and selector, an add action, and an X to return to the feed. The selector lists tabs vertically with wrapping titles, an explicit current-tab check, and visible close controls. Each touch control uses the 44px size token. Desktop tabs keep their horizontal layout. Document links within the current task open through the router and retain the feed's reading position and query cache.

The redesigned task thread (flag enableTaskChatRedesign) is the first surface to tokenize motion. Principles — reasoning only; values live in ui/src/index.css:

  • One home, and it is :root, not @theme inline. @theme inline bakes literals at build time, so a value placed there cannot be moved at runtime. The dev tweak panel tunes motion by writing CSS custom properties live, so every motion token must resolve at runtime — hence :root.
  • Two tiers. Primitives (--motion-duration-*, --motion-ease-*) express the app's baseline motion feel; state/component-scoped tokens (--motion-<state>-*) reference the primitives so the whole thread retunes from a few knobs. Scoped tokens exist so the tweak panel can group controls by the state they affect.
  • Reuse the house curves. New easing defaults point at the two curves already used across the app rather than inventing a third feel.
  • No hardcoded timing in components. Durations, easings, delays, and staggers used by the redesigned thread must reference these tokens; a check script rejects raw ms / cubic-bezier values outside ui/src/index.css. This discipline is what makes the tweak panel structurally possible.
  • Values are placeholders. The committed numbers are sensible starting points, tuned live by a human and pasted back from the tweak panel's export — never treated as final during the baseline build.
  • Reduced motion is honored at the token layer. A prefers-reduced-motion: reduce block collapses the duration/stagger tokens to zero, cascading to every scoped token, in addition to each animation's own component-level guard.

Agent Chat and regular task chats keep pending questions as compact “Unanswered question” entries at their original position in history. Dismissing the form or sending a newer user message clears it from the composer without resolving it. Questions never contribute to composer pending counts or navigation. Opening the history entry restores the original form and its draft; submitting later uses the same durable question response path. Actual permission reviews retain their permission checks.