feat(threads): add grid layouts and auto-hide composers (#3626)

This commit is contained in:
José Almeida
2026-09-13 14:13:10 +02:00
committed by GitHub
parent 88d73abaad
commit f718c50666
92 changed files with 6624 additions and 582 deletions
@@ -1,5 +1,5 @@
---
status: draft
status: active
system: ui
created: 2026-08-28
owners:
@@ -11,8 +11,9 @@ owners:
## Overview
Threads gives a user one horizontally scrollable view of current task
conversations. A task can have several agent sessions, and its column can
switch among those existing sessions. Only an explicit pending action marks a
conversations, arranged in full-height columns or a two-row grid. A task can
have several agent sessions, and its tile can switch among those existing
sessions. Only an explicit pending action marks a
question or permission. The deck must show accurate attention states and stay
usable when a workspace has many active tasks.
@@ -22,13 +23,16 @@ platform owns the bounded status and session-stream delivery contract in
## Terminology
- **Task column:** The stable Threads shell for one task.
- **Task column / tile:** The stable Threads shell for one task. A grid tile
has the same identity and session-selection behavior as a full-height column.
- **Selected session:** The existing agent session whose conversation is shown
in a task column.
- **Attention action:** An explicit pending clarification or permission for a
session.
- **Detail-active column:** A task column that can mount its selected session's
full conversation under the platform delivery budget.
- **Auto-hide composer:** A presentation preference that collapses routine
reply controls until hover or keyboard interaction on pointer-based layouts.
## Requirements
@@ -157,6 +161,114 @@ desktop and mobile navigation.
or transcript loading. Reversing the swipe shall restore the indicator for
the nearest thread. Loading completion alone shall not change its position.
### REQ-UI-THREADS-DECK-004: Conversation layouts
**Intent:** Let users inspect twice as many conversations at a readable width
without changing which tasks or sessions they are following.
#### Acceptance criteria
- **AC-UI-THREADS-DECK-004.1:** Columns shall use the existing full-height,
horizontally scrolling arrangement. Grid shall use two equal-height rows
at the same minimum readable tile width, with horizontal scrolling for
additional tasks and independent vertical transcript scrolling in each tile.
- **AC-UI-THREADS-DECK-004.2:** Grid shall place the stable task sequence from
top to bottom within each visual column, then proceed to the next column.
An odd final task shall occupy the upper tile without duplication. A single
admitted task shall fill the available height in either selected layout.
- **AC-UI-THREADS-DECK-004.3:** Changing layout shall retain the admitted task
set, stable order, selected session per task, and existing reply drafts. It
shall keep the last interacted surviving task visible, falling back to the
current visible task and then the first admitted task when needed.
- **AC-UI-THREADS-DECK-004.4:** A reply or status update shall not move a task
to another tile. Removing a task, changing filters, or following a deep link
shall use the existing deterministic admission and recovery behavior in
either layout; a lower-row task shall be directly reachable.
- **AC-UI-THREADS-DECK-004.5:** Both visible grid rows shall show their selected
live conversations. Offscreen tiles and unselected sibling sessions shall
remain subject to the existing platform delivery budget. Hovering a composer
shall not add a session subscription or mark a conversation read.
- **AC-UI-THREADS-DECK-004.6:** Below the phone breakpoint the system shall
show one full-width, full-height conversation with the existing swipe,
position indicator, and thread picker. A saved Grid preference shall be
retained for wider displays without mounting a hidden desktop grid.
- **AC-UI-THREADS-DECK-004.7:** When the available board content height cannot
fit two 300-CSS-pixel tiles plus their gap, the system shall use Columns and
explain that Grid needs more height. Increasing the available height shall
restore Grid without changing the saved preference or task selection.
- **AC-UI-THREADS-DECK-004.8:** Both layouts shall retain the existing Open
task, task-action, and session-selection controls. Layout selection shall not
add an in-place enlargement mode or change Open task navigation.
### REQ-UI-THREADS-DECK-005: Composer disclosure
**Intent:** Reclaim transcript space while monitoring conversations and keep
normal replying, attention handling, and agent controls reachable.
#### Acceptance criteria
- **AC-UI-THREADS-DECK-005.1:** With auto-hide disabled, each detail-active
conversation shall retain its normal composer. With auto-hide enabled and
no ongoing interaction, draft, or required action, routine composer content
and controls shall collapse, leaving only the existing CI popover trigger
when applicable and increasing the visible transcript height. No Reply,
Stop, plugin, toolbar, or other routine footer controls shall remain visible
in the collapsed state. With no CI status, the footer shall occupy no space.
- **AC-UI-THREADS-DECK-005.2:** On a fine pointer, dwelling over a task tile
shall reveal that tile's composer without taking keyboard focus. A pointer
crossing a tile for less than 150 milliseconds shall not reveal it. After
leaving for 300 milliseconds, it shall collapse only when no keep-open
condition remains.
- **AC-UI-THREADS-DECK-005.3:** Keyboard focus on a task tile shall reveal its
composer, allowing normal navigation to the editor and controls. Explicit
editor-focus actions shall reveal before focusing the editor. Collapsed controls shall not
remain invisible tab stops or appear as active controls to assistive tools.
- **AC-UI-THREADS-DECK-005.4:** While an editor has focus, unsent text or
attachments, a host-owned upload or send in progress, or a focused owned
menu, picker, or dialog, auto-hide shall keep that composer visible.
Hiding shall not unmount the editor or its plugin buttons, cancel a plugin
operation, or revoke its composer capability. Plugin-specific activity is
not required to be observable by the host; plugin controls hide with the
rest of the composer when no host-observable hold remains.
Session changes shall not transfer these states or drafts to another session.
- **AC-UI-THREADS-DECK-005.5:** Pending clarification or permission actions
and actionable session recovery shall remain visible and usable without
hover. Required composer content shall stay expanded until that condition
clears. Agent Stop shall remain available through the revealed composer and
the existing full task page, using the same cancellation-pending feedback.
- **AC-UI-THREADS-DECK-005.6:** Revealing or collapsing a composer shall not
resize neighboring tiles or scroll the board. A transcript following the
latest output shall continue to do so; a user reading history shall retain
the visible message and its offset, subject only to scroll-range clamping.
The editor and final messages shall remain reachable in a short grid tile.
- **AC-UI-THREADS-DECK-005.7:** Phone and coarse-pointer layouts shall retain
the normal visible composer, without overwriting the saved auto-hide
preference. On pointer-based layouts, explicit Collapse shall preserve the
draft and return focus to the task tile without immediately reopening it.
A later tile entry or explicit keyboard reveal shall restore the composer.
Required actions and host-owned sends/uploads shall prevent collapse and
explain why.
- **AC-UI-THREADS-DECK-005.8:** On phones, composing shall remain inside the
single conversation surface. The software keyboard, safe areas, long text,
and composer menus shall not cover the active reply action or create
document-level horizontal overflow. New touch controls shall have hit areas
of at least 44 by 44 CSS pixels.
- **AC-UI-THREADS-DECK-005.9:** Hover, disclosure, and layout changes shall
preserve normal model, mode, attachments, queue/steering, and submission
behavior. They shall neither start an agent nor submit, discard, or clear a
prompt. Full task pages and other chat hosts shall keep their existing
composer behavior.
- **AC-UI-THREADS-DECK-005.10:** Leaving the detail window shall release the
chat even if its composer was expanded. Returning shall restore the existing
session draft without widening the detail window. Transient hover, menu,
and focus state shall not be restored or persisted as view settings.
- **AC-UI-THREADS-DECK-005.11:** Composer reveal and collapse shall smoothly
change the allocated height and fade routine content within 250 milliseconds
after the existing disclosure delay. A reversed interaction shall continue
from the current visual position. CI shall remain visible and interactive
throughout. Reduced-motion users shall receive an immediate change; initial
page rendering and always-visible touch composers shall not animate.
## Out of scope
- Creating, deleting, renaming, reordering, or changing the primary session
@@ -165,6 +277,9 @@ desktop and mobile navigation.
- Copying transcripts or an unbounded session list into a task status summary.
- Replacing the full task workbench or its independent agent-tab behavior.
- Full DOM virtualization of lightweight task-column shells.
- Adjustable row counts, independent tile resizing, masonry, drag reordering,
and a new in-place expansion mode.
- Multiple simultaneous phone transcripts or a hover-only touch workflow.
Task-column scope, filters, sort, limits, and saved view persistence are in
Task-column scope, filters, sort, limits, and saved presentation persistence are in
[Threads Saved Views](threads-saved-views.md).
@@ -1,5 +1,5 @@
---
status: draft
status: active
system: ui
created: 2026-08-31
owners:
@@ -11,7 +11,7 @@ owners:
## Overview
Threads can contain many live task conversations. Users need direct control of
which task columns appear, their order, and the maximum number of columns.
which task conversations appear, their order, and the maximum number of chats.
Threads saved views provide this control without changing sidebar task views.
The UI system owns the interaction and query presentation. The task system
@@ -20,13 +20,15 @@ continues to own task and workflow state.
## Terminology
- **Threads view:** A named user setting that contains a task scope, filters,
sort rules, and an optional column limit.
sort rules, an optional chat limit, and presentation preferences.
- **Chat limit:** The maximum admitted tasks, historically called the column
limit. Two grid rows do not multiply this limit.
- **Task scope:** Either all eligible Threads tasks or an explicit set of task
IDs.
- **Eligible task:** A non-archived task in the active workspace that satisfies
the existing Threads deck eligibility contract and has a primary session.
- **Admitted task:** An eligible task that matches the active view and is inside
its current column limit.
its current chat limit.
- **View draft:** Unsaved changes to the active Threads view.
## Requirements
@@ -48,10 +50,11 @@ Threads views, so that I can change focus with one action.
system shall provide and select one canonical `All threads` view.
- **AC-UI-THREADS-SAVED-VIEWS-001.3:** `All threads` shall use all eligible
tasks, the existing attention-first order, and a default limit of five
columns.
chats.
- **AC-UI-THREADS-SAVED-VIEWS-001.4:** A user shall be able to create, rename,
delete, switch, overwrite, duplicate with `Save as`, and discard changes to
Threads views.
Threads views. Switching or reselecting a view shall not silently discard
an active draft; Save or Discard is required first on desktop and touch.
- **AC-UI-THREADS-SAVED-VIEWS-001.5:** A user shall be able to save at most 50
Threads views. The UI shall explain the limit before another create action.
- **AC-UI-THREADS-SAVED-VIEWS-001.6:** Saved views, the active view, and the
@@ -104,10 +107,10 @@ bounded task data.
- **AC-UI-THREADS-SAVED-VIEWS-002.13:** Filters shall support active errors,
task labels, task origin, and tasks that have more than one agent session.
### REQ-UI-THREADS-SAVED-VIEWS-003: Sort and column limit
### REQ-UI-THREADS-SAVED-VIEWS-003: Sort and chat limit
**Intent:** Give the user a predictable order and a hard bound on simultaneous
task columns.
chats.
#### Acceptance criteria
@@ -117,9 +120,9 @@ task columns.
- **AC-UI-THREADS-SAVED-VIEWS-003.2:** Every sort shall use task ID as its final
deterministic tie-breaker.
- **AC-UI-THREADS-SAVED-VIEWS-003.3:** A Threads view shall support no user
column limit or an integer limit from 1 through 30.
chat limit or an integer limit from 1 through 30.
- **AC-UI-THREADS-SAVED-VIEWS-003.4:** The system shall apply task scope,
filters, sort, and then the column limit in that order.
filters, sort, and then the chat limit in that order.
- **AC-UI-THREADS-SAVED-VIEWS-003.5:** When the limit is 3, the board shall
mount at most three task-column shells for that view.
- **AC-UI-THREADS-SAVED-VIEWS-003.6:** The view control shall show the admitted
@@ -132,13 +135,13 @@ task columns.
- **AC-UI-THREADS-SAVED-VIEWS-003.9:** The user shall be able to reapply the
active sort to the current matching set without changing the saved view.
- **AC-UI-THREADS-SAVED-VIEWS-003.10:** A valid task deep link shall temporarily
admit and focus its task when the active view or column limit hides it.
admit and focus its task when the active view or chat limit hides it.
- **AC-UI-THREADS-SAVED-VIEWS-003.11:** A temporary deep-link admission shall
count toward the column limit and shall not modify the saved task scope.
count toward the chat limit and shall not modify the saved task scope.
- **AC-UI-THREADS-SAVED-VIEWS-003.12:** Each sort option shall include a
visible description that explains the order that the option applies.
- **AC-UI-THREADS-SAVED-VIEWS-003.13:** A new Threads view shall start with a
limit of five columns. The user can select another valid limit or no limit.
limit of five chats. The user can select another valid limit or no limit.
### REQ-UI-THREADS-SAVED-VIEWS-004: Responsive editor and recovery
@@ -177,6 +180,49 @@ touch screen.
- **AC-UI-THREADS-SAVED-VIEWS-004.13:** The desktop editor popover shall use a
visible border and elevation that separate it from the page background.
### REQ-UI-THREADS-SAVED-VIEWS-005: Saved presentation preferences
**Intent:** Let each saved view combine its task selection with an independent
layout and composer preference, using the existing view lifecycle.
#### Acceptance criteria
- **AC-UI-THREADS-SAVED-VIEWS-005.1:** Each view and view draft shall have
independent Columns/Grid and Auto-hide composer settings. Existing views
without these settings and newly created views shall use Columns with
auto-hide disabled.
- **AC-UI-THREADS-SAVED-VIEWS-005.2:** Presentation edits shall preview
immediately through the existing persisted draft. Save, Save as, Duplicate,
Discard, reload, and another client shall preserve or restore them with the
same semantics as other view fields. A failed write shall use the existing
rollback and retry surface.
- **AC-UI-THREADS-SAVED-VIEWS-005.3:** A presentation-only edit, save,
discard, or live update shall not reapply task sorting, change admission,
clear the selected session, or change another listing surface's settings.
- **AC-UI-THREADS-SAVED-VIEWS-005.4:** The existing view editor shall expose a
Display section with visible descriptions for Columns, Grid, and Auto-hide
composer. Layout selection shall appear only inside that configurator, not
as a separate top-bar selector. Edits shall retain the existing draft,
dirty indicator, and Save/Discard actions.
- **AC-UI-THREADS-SAVED-VIEWS-005.5:** The existing phone/tablet view drawer
shall expose these settings. Phone copy shall explain that Grid applies
on larger screens and auto-hide applies to pointer-based layouts; phone and
coarse-pointer composers remain visible. A responsive fallback shall not
create a draft or write over either saved presentation preference.
- **AC-UI-THREADS-SAVED-VIEWS-005.6:** The view editor shall label the existing
admission limit Maximum chats and explain that it counts tasks across both
rows. The default remains five, valid explicit limits remain 1 through 30,
and no limit retains its existing meaning. Switching to Grid shall not
silently increase the limit.
- **AC-UI-THREADS-SAVED-VIEWS-005.7:** Missing or unrecognized stored
presentation values shall fall back independently to Columns and auto-hide
disabled without dropping valid task selection, filters, sort, or limits.
Invalid new presentation values shall be rejected without a partial save.
The layout and disclosure interactions are owned by
[Threads Conversation Deck](threads-conversation-deck.md),
`REQ-UI-THREADS-DECK-004` and `REQ-UI-THREADS-DECK-005`.
## Out of scope
- Sharing one saved-view collection between Threads and the task sidebar.
@@ -1,10 +1,12 @@
---
status: draft
status: current
system: ui
requirements:
- REQ-UI-THREADS-DECK-001
- REQ-UI-THREADS-DECK-002
- REQ-UI-THREADS-DECK-003
- REQ-UI-THREADS-DECK-004
- REQ-UI-THREADS-DECK-005
---
# Threads Conversation Deck System Design
@@ -18,8 +20,8 @@ copy session selection into global task-page state and does not make Threads a
session-management surface.
[Threads Saved Views](threads-saved-views.md) owns the task query, admitted
task set, saved order, and optional column limit. This design applies after
that query admits a task shell.
task set, saved order, optional chat limit, and persistence of presentation
preferences. This design applies after that query admits a task shell.
The paired platform design owns network hydration, compact status events, and
full session subscription limits. This UI design owns the task-column shell,
@@ -33,6 +35,8 @@ and deep-link behavior.
| `REQ-UI-THREADS-DECK-001` | [Session membership and selection](#session-membership-and-selection), [Header controls](#header-controls), [Deep links](#deep-links) |
| `REQ-UI-THREADS-DECK-002` | [Status projection](#status-projection) |
| `REQ-UI-THREADS-DECK-003` | [Viewport activation](#viewport-activation), [Responsive behavior](#responsive-behavior), [Failure and recovery](#failure-and-recovery) |
| `REQ-UI-THREADS-DECK-004` | [Layout composition](#layout-composition), [Layout transitions](#layout-transitions) |
| `REQ-UI-THREADS-DECK-005` | [Composer disclosure](#composer-disclosure), [Composer geometry](#composer-geometry) |
## Components and responsibilities
@@ -45,7 +49,7 @@ and deep-link behavior.
a transcript or an unbounded session array.
- `ThreadsBoard` keeps every lightweight task-column shell in stable order and
observes its horizontal viewport.
- A new `useThreadColumnActivation` hook produces two sets. The preload set
- `useThreadColumnActivation` produces two sets. The preload set
contains visible columns plus at most one adjacent column on each side. The
detail set contains only intersecting desktop columns. On phone it contains
only the nearest snapped column. The same viewport owner exposes a separate
@@ -54,9 +58,9 @@ and deep-link behavior.
- `ThreadColumn` always renders the task shell. It mounts the session-list
adapter only while preloaded and mounts `ThreadConversation` only while
detail-active.
- A new `useThreadSessionSelection` helper owns one local selected session ID
per task column. It applies deterministic initial and removal fallback rules
without using `tasks.activeSessionId`.
- `ThreadColumn` owns one local selected session ID. `selectThreadSessionId`
applies deterministic initial and removal fallback rules without using
`tasks.activeSessionId`.
- `ThreadSessionSwitcher` renders compact desktop tabs or a mobile picker from
the same session view model.
- `ThreadConversation` mounts `TaskChatPanel` only for the selected session and
@@ -256,6 +260,220 @@ current mounted phone thread or the first remaining column, without scrolling.
Leaving phone layout closes the thread picker; returning does not reopen it.
Long labels truncate inside controls without document-level horizontal overflow.
## Layout composition
The `ThreadView` and matching draft gain `layout: "columns" | "grid"` and
`autoHideComposer: boolean`, through the saved-view persistence design.
`queryThreadView(...).effectiveView` is the page's single presentation source.
Pass its fields to `ThreadsBoard`; do not derive another active view inside
individual tiles. Layout and disclosure do not enter `queryFingerprint`.
Keep each `ThreadColumn` as a direct board child keyed by task ID. Its existing
`data-thread-column-id` remains the registry, selection-recovery, context-menu,
and deep-link boundary. Do not introduce per-pair wrappers or change React
keys when layout changes. Use ordinary labelled sections, not ARIA grid roles
that would impose spreadsheet keyboard behavior on a conversation.
Columns retains the existing flex arrangement. Grid uses CSS grid with
column flow, two `minmax(0, 1fr)` row tracks, and
`ceil(admittedTaskCount / 2)` equal flexible column tracks with the existing
360px minimum. The board owns horizontal overflow and keeps its existing
12px desktop gap and padding. Each tile owns its transcript's vertical scroll.
The sequence A, B, C, D produces A/C on the upper row and B/D on the lower row;
DOM, keyboard, picker, and screen-reader sequence remains A, B, C, D.
For one task, the effective row count is one. With an odd number greater than
one, the last lower cell stays empty; the last task is not duplicated or
stretched across rows. With no tasks, reuse the existing empty/loading state
and keep view controls available. `maxColumns` still counts admitted tasks;
it never becomes a grid-column count.
Use `useResponsiveBreakpoint().isMobile` for the phone boundary. Phones render
the existing snap deck and one detail-active conversation regardless of the
saved layout. Tablets and wider touch displays may render Grid but retain
normal visible composers. Width and pointer precision are separate inputs.
A small `thread-layout.ts` helper derives effective layout, row count,
and track count from saved layout, phone mode, task count, and available board
content height. A board-owned `ResizeObserver` measures content height after
padding; two rows require at least `2 * 300 + 12` CSS pixels. Below that bound,
render Columns. Keep Grid selected as the stored choice, and show a localized
reason in its layout control/editor. This is render-only fallback: hydration,
resize, keyboard appearance, and breakpoint changes never save preferences.
Before height is measurable, use Columns as a conservative render placeholder.
### Layout transitions
Extend `useThreadSelectionRecovery` with the effective layout identity so its
existing before/after geometry path also handles composition changes. Retain
the last interacted surviving task; if it is no longer a usable anchor, use
the current visible task, then the first admitted task. Preserve its horizontal
offset where the new scroll range permits, otherwise scroll only enough to
reveal it. Keep input focus when the same editor remains mounted. The task
ID, local selected session, and session draft are never keyed by row/column.
The board's `IntersectionObserver` still observes individual task tiles, so
both grid rows become detail-active. Preserve the existing preload limit of
visible tasks plus at most one neighboring task ID on each side of the stable
sequence; do not preload an entire extra grid column. Layout changes must
refresh geometry even if admitted IDs are identical. Update activation with an
effective-layout generation and reject callbacks from a disconnected observer
or replaced shell. Publish measured shell intersections synchronously when
rebuilding observation, retaining still-visible chats while dropping offscreen
ones before asynchronous delivery. Reconcile without briefly activating every
task, and preserve the one-phone-detail rule throughout responsive transitions.
Callback refs reconcile membership on the existing observer; only reflow or
board replacement rebuilds it, so removing a tile preserves surviving readers.
Keep the short-height explanation in Display, outside the measured board
allocation, so crossing the Grid threshold is independent of resize history.
Pointer, focus, and wheel input all record the interacted task for recovery.
Deep-link scrolling is instant when reduced motion is requested.
Recovery skips zero-offset scroll assignments so it does not cancel an
in-flight browser deep-link scroll when the first size measurement arrives.
Deep-link retirement uses the raw workspace/task/session request identity.
`useThreadFocusRequest` retains the initial activation fallback after reader
interaction until the target leaves the deck; failed archive readmission must
not revive a consumed mark or fallback.
The selected task remains the owner for context menus and removal recovery in
the lower row. `scrollIntoView` keeps `block: "nearest"`, and board scroll
recovery remains horizontal. Open task still routes through the page's existing
`handleOpenTask` callback. Hover does not call any focus/read/session API, and
`ThreadConversation` retains `embedded` and `isVisible={false}`.
## Composer disclosure
Use an optional Threads-only disclosure context, scoped to the task tile and
selected session, with a null default for other chat hosts. The
`use-composer-disclosure.ts` owns timing and interaction state; its provider
adds no DOM wrapper between the board and the stable task section. Tile
pointer/focus events feed this controller, while the selected chat reports
draft, operation, and required-action state from their existing owners.
Keep the input and plugin trees mounted while detail-active. Collapse one
routine region with a CSS grid-track transition, `aria-hidden`, and inert
interaction; do not duplicate `ChatInputArea`. The only persistent footer
content is the existing CI status/popover family in `ComposerCIStatus`: `PRStatusChip`,
`MRStatusChip`, `AzureDevOpsTaskPullRequestChip`, and
`RegisteredChangeRequestStatus`. Keep these instances in place so disclosure
does not remount a CI popover or add a background-sync consumer. An empty CI
row has no padding or height. No compact Reply/Stop/plugin row is added.
When auto-hide is effective, `ChatInputArea` places that one CI family outside
`ComposerDisclosureRegion`; `ChatStatusBar` omits its usual inline CI family.
This branch depends on the effective preference, not transient expansion, so
hover never remounts CI. Hints, routine status, queue, editor, and toolbar share
one animated region, avoiding independently snapping margins or padding.
All other status controls, queue affordances, context controls, editor, and
toolbar collapse together. Stop remains in the normal composer; reuse its
existing eligibility and cancellation-pending behavior without another sender
or cancel action. Existing task/status header cues remain. Pending
clarification, selected-session permission, or actionable recovery forces the
normal input area open, keeping `DynamicRouteRecovery`, recovery banners, and
clarification content in their current owners. Do not create a second action
renderer from task summary data. Do not assign another sibling's task-wide
pending action to this composer.
### Disclosure state and event ownership
| State/input | Effect |
| --- | --- |
| Auto-hide off, phone, or coarse pointer | Normal input area; no hover timers; preserve the saved preference. |
| Fine-pointer dwell | Open after 150ms; leaving before then cancels. Never focus the editor. |
| Pointer leaves | Close after 300ms only if no interaction, draft, busy operation, or required action holds it open. |
| Tile receives keyboard focus | Reveal without stealing focus; normal Tab navigation reaches its composer. |
| Explicit editor focus | Reveal before `ChatInputContainerHandle.focusInput` or the existing plugin composer focus operation. |
| Editor/owned overlay focus | Hold open, including React-owned portals outside the tile's DOM bounds. |
| Unsent text/attachments or host-owned upload/send | Hold open for automatic hiding. An existing stored draft also opens on detail activation. |
| Required clarification/permission/recovery | Force open without introducing a new autofocus or agent action. |
| Explicit Collapse | Available inside the revealed composer; retain draft and focus the task section with `preventScroll`. Suppress that focus event's immediate reopen and cancel the current pointer-entry dwell. A new pointer entry can reveal again. Block for a host-owned operation or required action. |
| Session change/detail deactivation | Cancel timers and overlay holds; release the normal chat subscription and retain only the existing session-keyed draft. |
Explicit collapse suppresses draft-driven reopening until a new fine-pointer
entry, explicit keyboard reveal, or required action. It never clears a draft.
The task section is a keyboard focus target only when auto-hide is effective;
Enter on that section can reopen an explicitly collapsed composer. Child
controls keep their own Enter behavior. Phone and coarse-pointer composers
stay visible because the user requested no persistent Reply control. Use the
existing breakpoint/pointer model and ignore touch compatibility hover events.
`ChatInputContainer` already receives `value`, attachments, upload state, and
input focus from `useChatInputContainer`/`useChatInputState`. Expose a narrow
optional `useComposerActivity` report through that context from the native
owner; do not infer drafts by
polling `getValue()`, duplicating draft storage, or querying the DOM. Use
React focus capture across owned portals and existing controlled open-change
callbacks where a host overlay deliberately does not move focus. Any local
hold aggregation remains session-scoped, never a global overlay registry.
Release holds on close, failure, session change, and unmount. Parent `:hover`
or DOM `contains()` alone cannot represent portal ownership.
Plugin buttons hide with the composer, with no visibility exception. Their
components and existing `PluginComposerCapability` remain live while hidden;
do not infer recording state, cancel an operation, or revoke a capability on
collapse. This implements the user's CI-only direction without a new public
plugin activity API or an external voice-plugin change. Host-observable focus
and drafts still keep the composer open; opaque plugin activity alone is not
a keep-open condition. The existing session-change/unmount revocation remains.
Collapsed editors must not autofocus from mount/recovery effects. Reveal
before an existing explicit focus request is fulfilled. Keyboard focus and
the focusable tile make the feature usable without pointer hover. Escape belongs to
an open child menu/dialog first; collapsing the composer must never dismiss
the parent task surface. Auto-hide and explicit collapse cannot call submit,
cancel, clear, start, or stop as incidental side effects.
### Composer geometry
Composer disclosure changes only the transcript/input allocation inside the
fixed task tile. It must reclaim actual height; opacity-only hiding leaves the
space occupied, and overlaying the editor covers the latest message. Use the
existing grid-track disclosure pattern: `0fr` to `1fr`, a zero-minimum clipped
inner wrapper, and a 6px vertical offset with opacity on the routine content.
CSS transitions take 200ms to reveal and 160ms to collapse with eased motion;
they retarget from the current position on interruption. Do not animate CI,
mount/unmount the editor, introduce animation timers, or use `transition: all`.
Remove inert immediately on reveal, apply it immediately on collapse, and defer
visual `visibility: hidden` until exit finishes. `prefers-reduced-motion`
disables transitions and the visibility delay. Outside effective auto-hide,
wrappers use normal layout without motion. Initial render does not animate.
Use the existing native transcript scroll owner in
`message-list-native-scroll.ts`, `transcript-auto-scroll.ts`, and
`clamped-scroll-restore.ts`. Its `transcript-viewport-resize.ts` helper observes
height-only changes for a visible, initially placed, auto-following transcript.
It retains the pre-resize bottom state even when native clamping emits a scroll
event before resize delivery. Width reflow, history, frozen scroll, and an
active programmatic scroll lock keep their existing owners.
On footer allocation changes, preserve its current
follow/reading policy. A following viewport stays at the bottom; a history
reader retains the first visible message and offset, clamped to the new scroll
range. Add a narrowly scoped resize/anchor seam there only if the existing
resize path does not preserve the required position. Do not add a competing
scroll loop to `ThreadColumn` or change the stored auto-scroll preference.
The footer's expanded maximum height is the tile body height minus an 80px
transcript floor. Long expanded content scrolls within that bounded footer;
the input and its actions stay reachable. Apply this bound to Threads even
when auto-hide is off. The editor's ordinary
internal text scroll and existing portal menus continue to work. Both regions
use `min-h-0`; no content can increase the grid row height. Pending question
content can scroll inside the bounded footer without clipping its response
actions. Phones use the existing dynamic-height app shell and keyboard/safe-
area handling, with 44px touch actions; the composer is inline, not a second
chat drawer. Readable transcript and reachable actions take precedence over
animation.
Session draft text, rich content, and staged attachments continue to use
`useChatInputState` and `getChatDraftText`/`getChatDraftContent`/
`getChatDraftAttachments`. They remain transient session drafts, distinct from
backend-owned view preferences. When offscreen detail unmounts, existing draft
restoration owns recovery; an expanded composer never pins an offscreen stream.
Queue transitions must preserve the mounted input and focus. Keep the keyed
input as `QueueAffordance`'s single child, with the collapse action outside that
wrapper, so adding or draining queued messages cannot remount the editor.
## Failure and recovery
- Before membership loads, a preloaded shell shows a local session-loading
@@ -294,3 +512,11 @@ prove the one-phone-transcript budget.
- [Viewport Activation Owns Threads Session Streams](../../../decisions/2026-08-28-viewport-activation-owns-thread-streams.md)
- [Separate Task Summary and Session Stream Traffic](../../../decisions/2026-08-01-separate-task-summary-session-stream-traffic.md)
## Implementation plans
- [Threads layouts and composer disclosure](../../../plans/threads-layouts/plan.md)
implements the draft layout/disclosure additions. Existing deck/session and
mobile behavior remains covered by the completed
[windowing](../../../plans/threads-multi-session-windowing/plan.md) and
[mobile](../../../plans/mobile-threads/plan.md) packages.
@@ -1,11 +1,12 @@
---
status: draft
status: current
system: ui
requirements:
- REQ-UI-THREADS-SAVED-VIEWS-001
- REQ-UI-THREADS-SAVED-VIEWS-002
- REQ-UI-THREADS-SAVED-VIEWS-003
- REQ-UI-THREADS-SAVED-VIEWS-004
- REQ-UI-THREADS-SAVED-VIEWS-005
---
# Threads Saved Views System Design
@@ -14,7 +15,9 @@ requirements:
This design adds user-owned task queries to `/threads`. A query controls the
task scope, filter clauses, sort, and maximum admitted columns. The existing
Threads deck continues to own session selection and viewport activation.
Threads deck continues to own session selection, viewport activation, layout
composition, and composer disclosure. This design owns persistence of its
presentation preferences with each view.
Threads and the task sidebar share query primitives. They keep separate saved
view collections because their presentation fields and active-view lifecycles
@@ -31,6 +34,7 @@ Stored task IDs never grant access to a task or cause a direct task fetch.
| `REQ-UI-THREADS-SAVED-VIEWS-002` | [Candidate projection](#candidate-projection), [Filter catalog](#filter-catalog), [Query pipeline](#query-pipeline) |
| `REQ-UI-THREADS-SAVED-VIEWS-003` | [Sort, admission, and stable order](#sort-admission-and-stable-order), [Deep links](#deep-links) |
| `REQ-UI-THREADS-SAVED-VIEWS-004` | [Top-bar controls](#top-bar-controls), [Responsive behavior](#responsive-behavior), [Failure and recovery](#failure-and-recovery) |
| `REQ-UI-THREADS-SAVED-VIEWS-005` | [Presentation preferences](#presentation-preferences) |
## Saved view state
@@ -48,6 +52,8 @@ type ThreadView = {
filters: ThreadFilterClause[];
sort: ThreadSortSpec;
maxColumns: number | null;
layout: "columns" | "grid";
autoHideComposer: boolean;
};
type ThreadViewDraft = Omit<ThreadView, "id" | "name"> & {
@@ -68,7 +74,7 @@ keys remain unchanged.
The store reuses the sidebar action semantics:
- view selection persists immediately;
- view selection persists immediately when there is no active draft;
- edits create or update a persisted draft;
- Save replaces the active saved definition and clears the draft;
- Save as creates and activates another view;
@@ -76,6 +82,12 @@ The store reuses the sidebar action semantics:
- create, rename, and delete use optimistic updates with rollback;
- the 50-view limit is checked in the UI and backend.
While a draft exists, both view pickers disable saved-view selection and show
a localized explanation directing the user to View settings. The existing
Save/Discard actions clear the draft before switching. `setThreadActiveView`
also rejects selection while a draft exists, including same-view selection,
so a stale UI event cannot clear an unsaved draft.
## Persistence and synchronization
The existing `users.settings` JSON object adds these portable fields:
@@ -100,7 +112,9 @@ The existing `users.settings` JSON object adds these portable fields:
},
"filters": [],
"sort": { "key": "attention", "direction": "asc" },
"max_columns": 3
"max_columns": 3,
"layout": "grid",
"auto_hide_composer": true
}
```
@@ -120,13 +134,88 @@ The backend rejects these invalid values:
- an empty view name;
- more than 20 filter clauses in one view;
- more than 200 selected task IDs in one view;
- a numeric column limit outside 1 through 30;
- a numeric chat limit outside 1 through 30;
- an active or draft base ID that does not identify a saved view.
The user-settings revision orders boot hydration, PATCH responses, and live
events. The frontend rejects older settings snapshots. A queued settings sync
serializes rapid view mutations and preserves unrelated user settings.
## Presentation preferences
`layout` and `auto_hide_composer` belong to both saved views and drafts. The
backend `models.ThreadView` and `models.ThreadViewDraft` add these fields; DTOs
already carry those model types. Extend the existing service validation and
store normalization rather than creating another settings endpoint or table.
The request/response shape remains the same through PATCH, boot hydration,
and `user.settings.updated`.
The native boot projection in `backendapp/boot_thread_views.go` also maps
`layout` and `autoHideComposer` for both views and drafts. It emits the
frontend's camel-case state directly, so the API wire mapper cannot repair
fields omitted there.
The backend supplies `columns` and `false` for omitted presentation fields in
stored definitions, canonical defaults, new views, and legacy draft payloads.
It normalizes unknown stored presentation values independently without losing
the rest of the view. Decode these new stored fields tolerantly at the existing
`scanUserSettings` JSON boundary before constructing their typed values; a
malformed new field must not make the whole user-settings read fail. Normal
request decoding and service validation reject explicitly unsupported new
values before persistence. Preserve existing top-level PATCH semantics:
omitted collections/drafts are unchanged, a null draft clears it, and supplied
view lists are complete replacements. Missing nested fields in such a
replacement use the compatibility defaults; they are not a partial view patch.
Keep `DefaultThreadViews` as the backend default owner. Extend the one frontend
wire mapper in `thread-view-wire.ts` for `layout` and `autoHideComposer`, using
the same compatibility fallbacks. `createDefaultThreadView` supplies only the
matching render/new-view shape; it does not write defaults during hydration.
No browser storage fallback or DB schema migration is introduced.
Update every view/draft constructor and clone in `thread-view-actions.ts`,
including optimistic before/after snapshots, failed-write retry payloads,
deferred server state, overwrite, Save as, and Duplicate. Extend
`updateThreadViewDraft` and its exported slice type. A filter-only draft must
carry its base view's presentation fields; a presentation-only patch must
retain all existing query fields. The draft merge in
`thread-view-query.ts:cloneViewWithDraft` returns the effective presentation.
Its existing `queryFingerprint` explicitly selects query fields; keep the new
presentation fields out so editing, saving, rollback, and remote presentation
updates do not reorder task tiles. Real view selection/query edits retain
their existing reset behavior.
Add a Display section to `ThreadsViewEditor`, reusing shared Select/Switch and
the existing Save/Save as/Discard footer. Fields are Layout (Columns/Grid) and
Auto-hide composer. Descriptions explain full-height reading, two-row
monitoring, CI-only collapsed footers, hover/keyboard disclosure, and the
visible-composer touch fallback. Keep layout selection exclusively inside
Display in the existing configurator, with its draft and Save/Discard flow.
The top bar does not duplicate the layout selector.
Keep ordinary desktop controls at 28px and touch controls at least 44px.
Phone/tablet use the existing view drawer and its internal editor page. Phone
layout controls edit the saved wider-screen choice and explain why the phone
continues to show one chat. Auto-hide's helper explains that phone and coarse
pointers retain their normal composer without changing the saved preference.
The grid-height fallback is derived by the board,
with a reason made available to the layout control/editor; it never changes
the draft. Preserve one inset drawer, its header/back controls, one body scroll
owner, safe-area padding, and existing error/retry presentation.
Relabel the editor's `maxColumns` field Maximum chats across all locales,
without changing the internal or wire name. The helper explains that five
means five task tiles in either layout, with the existing hidden-match count.
No automatic new view, limit increase, global preference, or default auto-hide
activation accompanies choosing Grid.
All copy uses `threads` translations. Update English, Portuguese, Simplified
Chinese, the generated Traditional Chinese pair, and pseudo output with the
repository translation tools. Existing synchronized settings revisions,
authorization, rollback, and retry remain the failure/consistency boundary.
The implementation package must test a second client's update and a rejected
rapid edit, not only reload serialization.
## Candidate projection
`selectThreadCandidates` replaces direct construction of the final deck list.
@@ -236,7 +325,7 @@ uses activity recency inside each group. Plain `WAITING_FOR_INPUT` does not
become a person-action rank.
The query fingerprint contains the active view ID, effective scope, filters,
sort, and column limit. A fingerprint change or explicit Reapply sort action
sort, and chat limit. A fingerprint change or explicit Reapply sort action
increments the order-reset generation. The next render uses the complete
sorted order.
@@ -267,16 +356,20 @@ session membership.
place the slot before `ViewToggleGroup`. Phone groups the page label and active
view in the title control, with inline pagination and no scrolling action strip.
Threads supplies two compact desktop/tablet controls:
Threads supplies the existing view selector/settings controls:
```text
[All threads v] [View settings] [Kanban] [Pipeline] [Threads] [List]
[All threads v] [View settings] [Listing views]
```
The first control switches saved views and exposes New view. The second opens
the editor and shows a dirty indicator for a draft. Its accessible name
includes the active view and admitted count.
Layout selection stays inside Display as described in
[Presentation preferences](#presentation-preferences). Touch layouts keep
Display in the existing view drawer.
The existing `KanbanDisplayDropdown` does not render on Threads. Workflow,
repository, and task filters on that surface create a second query owner.
Other task-listing pages keep their current header controls.
@@ -370,3 +463,11 @@ return, and zero document overflow.
- [Surface-owned Saved Task Views](../../../decisions/2026-08-31-surface-owned-saved-task-views.md)
- [Backend-owned Portable User Settings](../../../decisions/0041-backend-owned-portable-user-settings.md)
- [Viewport Activation Owns Threads Session Streams](../../../decisions/2026-08-28-viewport-activation-owns-thread-streams.md)
## Implementation plans
- [Threads layouts and composer disclosure](../../../plans/threads-layouts/plan.md)
implements presentation preferences under `REQ-UI-THREADS-SAVED-VIEWS-005`.
- [Threads saved views](../../../plans/threads-saved-views/plan.md) records the
completed query, editor, and persistence foundation. Its completed results
do not claim coverage of the later presentation additions.