feat(threads): add grid layouts and auto-hide composers (#3626)
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user