190 lines
26 KiB
Markdown
190 lines
26 KiB
Markdown
# Workspace Chat polish — independent IA planning
|
||
|
||
Date: 2026-11-02
|
||
Role: independent IA planning; planning only
|
||
|
||
## Basis and boundaries
|
||
|
||
This review is derived from the approved scope and the code references below. The earlier role sections in `.convoys/workspace-chat-polish.md` are main-session draft proposals, **not independent review evidence or approved implementation contracts**. Reading them does not constitute endorsement.
|
||
|
||
Read: `AGENTS.md`, `docs/Glossary.md`, `.convoys/README.md`, the convoy manifest, and the routes, shell, sidebar, conversation, and supporting data contracts cited below. Applied the accessibility skill as planning guidance only. No agents, lifecycle operations, MCP mutations, implementation, or code checks were run. This document is not a runtime accessibility or security certification.
|
||
|
||
Approved boundary: human Chat; replace the Spaces sidebar only on Chat routes; retain a reusable Chat navigation surface for other hosts. Exactly **Favorites, Channels, DMs**. Flat Channels by default, with no space hierarchy, Defaults section, or New Message section. DMs Add selects one or many **other** people. Names/avatars, focus-visible actions, primary-composer editing, and modal deletion are in scope. AI/agent participation is not.
|
||
|
||
## 1. Current IA: evidence and consequences
|
||
|
||
Line references describe the inspected source, not future edits. Paths are repository-relative.
|
||
|
||
| Evidence | IA consequence |
|
||
| --- | --- |
|
||
| `apps/web/app/(app)/[workspaceSlug]/chat/page.tsx:1–10` and `chat/[channelId]/page.tsx:1–10` mount the same `ChatWorkspace`, with an optional channel ID. | Keep the existing index/detail route hierarchy. DMs already fit the channel detail route; no new DM route is needed. |
|
||
| `apps/web/components/layout/app-shell.tsx:43–55` always renders IconRail, Sidebar, main content, and RightPanel. `apps/web/components/chat/chat-workspace.tsx:733–742` adds another navigation aside inside main. | The duplicate sidebar is structural. The solution is one contextual navigation slot, not cosmetically shrinking one of two sidebars. Avoid nested main landmarks when extracting Chat. |
|
||
| `apps/web/components/layout/icon-rail.tsx:41–53` distinguishes AI and Chat. `apps/web/components/panels/right-panel.tsx:20–24` can display AI chat or object detail. | Human Chat must remain distinct from AI. A previously opened global panel can compete with the thread panel; host behavior needs an explicit decision. |
|
||
| `apps/web/components/sidebar/sidebar.tsx:19–32` wraps SidebarHeader/NavTree; `sidebar-header.tsx:63–96` labels the surface Home, routes Search through the global callback, and has a Filter control with no handler. | Reuse visual primitives, not these meanings. Chat search must not silently invoke object/global search; do not copy a nonfunctional filter affordance. |
|
||
| `apps/web/components/chat/chat-workspace.tsx:752–799` displays all nonarchived kinds together, labels every direct conversation “Direct message,” and links by channel ID. | Split Channels from DMs; participant identity is necessary to distinguish DM destinations. |
|
||
| `packages/database/src/chat/channel-drizzle-adapter.ts:17–25,72–95` projects ID/kind/name/owner/directKey/archive state, returns public channels or channels with caller membership, ordered by creation. | Visible is not synonymous with “joined.” There is no participant/name/avatar projection here, no last-message ordering field, and no space metadata. Do not promise recent-activity sorting or private-channel discovery. |
|
||
| `packages/database/src/schema/chat.ts:22–49,79–111` stores channels separately from objects and has workspace-scoped membership; direct names must be null at `:68–70`. | DMs are participant-derived labels, not renamed channels. Space grouping/linkage is not an existing channel capability. |
|
||
| `apps/web/server/routers/chat.ts:283–365` exposes list/create/rename/archive/private-membership/direct creation. `packages/shared/src/chat/index.ts:195–201` accepts 2–100 total DM participants. `packages/database/src/chat/manage-channels.ts:414–448` requires the caller, validates membership, canonicalizes the participant set, and returns an existing matching direct channel. | The people picker must hide self but add self to the submitted set. Existing group DMs can be reused without inventing a new conversation API. |
|
||
| `apps/web/server/routers/workspaces.ts:176–189` lists workspace member IDs, names, email, avatar URL, and role. | A candidate source exists for the DM picker. It does not supply per-conversation membership or resolve historical authors who are no longer members. Email must not become the default chat identity. |
|
||
| `apps/web/server/routers/favorites.ts:50–70,90–99` lists object metadata and takes `objectId`; `:16–47` checks object/workspace visibility. | The current favorites API is not channel-capable. Reusing its name does not supply channel authorization or channel targets. |
|
||
| `apps/web/components/chat/chat-message-list.tsx:8–20,92–108` has author ID but no profile projection and renders the raw ID. `:110–142` gates edit/delete controls. | Add safe identity projection without losing authorization behavior; do not substitute IDs as user-visible names. |
|
||
| `apps/web/components/chat/chat-workspace.tsx:891–951,986–990` renders separate edit/delete controls and the normal composer; `:994–1072` has a separate thread region/composer. `chat-composer.tsx:8–25` only accepts send/disabled/label and owns local text. | Composer edit mode requires a controlled state contract, including draft preservation and reply targets. This is not just moving the existing textarea. |
|
||
| `apps/web/components/chat/chat-workspace.tsx:70–100,111–121` revokes scope, purges cached messages and drafts, and redirects to Chat index on lost visibility. `:824–827,870–873` supplies unavailable/empty copy. | Preserve revocation safeguards through extraction. Distinguish unavailable, loading, failure, and truly empty states. |
|
||
|
||
### Independent assessment
|
||
|
||
The approved three-section model fits the existing kinds and routes without adding another entity hierarchy. The main IA dependencies are **recognizable conversation identity**, **channel-aware favorite authorization**, and **clear scope of discovery**. Styling alone cannot address them. The flat list is viable if search is explicitly conversation discovery, archived destinations remain recoverable, and a shortcut never becomes a second conversation record.
|
||
|
||
## 2. Proposed route and content hierarchy
|
||
|
||
```text
|
||
Workspace shell
|
||
├── Existing global header and rail
|
||
├── Contextual navigation slot
|
||
│ ├── Non-Chat routes: existing Spaces/object sidebar, unchanged
|
||
│ └── Chat routes: reusable Chat navigation surface
|
||
│ ├── Chat heading; Find conversations; Filter; Collapse
|
||
│ ├── Favorites [disclosure; existing-chat shortcut picker]
|
||
│ ├── Channels [disclosure; Create channel]
|
||
│ └── DMs [disclosure; Start conversation]
|
||
└── Main content
|
||
├── /[workspaceSlug]/chat
|
||
│ └── Select a conversation / useful empty guidance
|
||
└── /[workspaceSlug]/chat/[channelId]
|
||
├── Conversation heading: identity, kind/privacy, archive state
|
||
├── Message history: author, time, body, message actions
|
||
├── Primary composer: compose OR edit mode
|
||
└── Optional thread region: root context, replies, reply composer
|
||
```
|
||
|
||
- Treat route segments as an exact Chat boundary, including its detail descendants. Do not use loose substring checks or replace navigation globally.
|
||
- The selected conversation comes from the canonical route, not from a favorite row's identity. Back/forward and copied links keep working.
|
||
- The index does not create a default channel, automatically start a DM, or redirect to an arbitrary conversation. “Select a conversation” is better than the current channel-only wording.
|
||
- Search/filter/disclosure state changes navigation visibility, not the open conversation or read state. A hidden selected row does not mean access has been lost.
|
||
- Extract the navigation presentation and its workspace-scoped data ownership away from message history, subscriptions, and composers. A future host can provide workspace, selected conversation, and an open-conversation callback; these are **proposed UI responsibilities, not existing API signatures**. Route host uses canonical links; embedded hosts may handle selection locally.
|
||
- Non-Chat routes do not automatically gain Chat navigation. Reusability is a mounting capability, not a new global layout requirement.
|
||
- Narrow screens should switch between navigation and conversation, with an explicit back-to-conversations control; do not show several fixed-width sidebars simultaneously. Keep all three sections in the navigation view.
|
||
|
||
## 3. Section membership and action meanings
|
||
|
||
| Surface | Meaning and default contents | Add/action meaning |
|
||
| --- | --- | --- |
|
||
| Favorites | Current user's shortcuts to accessible Chat conversations in this workspace, both channels and DMs. No object/task/document favorites mixed into this surface. | **Add favorite** opens a chooser of existing accessible conversations. It creates neither a channel nor a DM. Unfavorite removes only the shortcut. |
|
||
| Channels | All accessible public/private nondirect channels, active by default, in one flat list. Public/private is secondary identity, not two extra sections. | **Create channel** requests name and public/private kind using the existing capability. Private initially means caller-only membership; do not imply recipients are invited automatically. |
|
||
| DMs | Existing accessible direct conversations, active by default, identified by the other participant(s). | **Start conversation** opens a human-member multiselect. One other person yields a 1:1 DM; more than one yields a group DM. The action does not send a message. |
|
||
|
||
All section disclosures are independent of their sibling action buttons. Use section-qualified accessible labels, not three indistinguishable “Add” labels. A More menu is optional: include it only for real, supported section-specific actions; an empty menu or placeholder “Manage” action adds ambiguity. Do not put create-channel actions under Favorites or add-people-to-existing-group under DMs Add.
|
||
|
||
### DM creation and identity
|
||
|
||
1. Open the member picker from DMs Add. Exclude the current user; show names and avatars, searchable within this workspace. Keep selected people visible as removable selections.
|
||
2. Require at least one other person; cap selection at 99 others under the existing 100-total schema. Deduplicate selected IDs and include the caller exactly once in the request.
|
||
3. Label submission **Start conversation**, show pending/error state, then open the returned channel ID. Reopening the same participant set is success, not a duplicate-error condition.
|
||
4. Membership of an existing direct conversation is immutable in the inspected adapter (`channel-drizzle-adapter.ts:31–35`). Starting with a different set is a different conversation, not editing membership of the original.
|
||
5. 1:1 title: the other person's display name/avatar. Group title: stable ordered other-participant names, truncated visually with a count and an accessible complete name; show the full participant list on demand. Do not derive participants from a directKey hash or owner ID.
|
||
6. Where names collide, use approved additional profile context or participant detail—not raw UUIDs. Where identity is unavailable, use a neutral avatar and “Unavailable member”; clarify the exact departed/deleted distinction only when the server can prove it. Never render “undefined,” an email address by default, or an AI/system identity invented for a null human author.
|
||
|
||
### Duplicate favorites decision
|
||
|
||
**Keep a favorite in its canonical Channels/DMs section as well as Favorites.** Favorites is a shortcut collection, not a move operation. This preserves complete discoverability when Favorites is collapsed and avoids items seemingly vanishing from their type section.
|
||
|
||
- One favorite record per user/conversation target. Both appearances share label, unread state, archive state, and navigation target; favorite toggling updates both.
|
||
- Both appearances may indicate the same current destination. Do not create competing selection state or double-count unread totals.
|
||
- Removing a favorite does not close the conversation, delete messages, leave membership, or remove its canonical row. Restore focus to a surviving nearby control if the shortcut row disappears.
|
||
- Show a concise empty hint (“Add a channel or DM to Favorites”). Avoid instructions that imply favoriting changes access.
|
||
- Prefer alphabetical display-name ordering with a stable internal ID tie-breaker for this slice. Apply it consistently within each section. Activity sorting and drag ordering are deferred because the inspected list contract does not support them. No UUIDs are displayed as tie-breakers.
|
||
|
||
## 4. Search, filters, and grouping
|
||
|
||
**Search label: “Find conversations.”** This is local navigation discovery, not message search, global object search, or a people directory.
|
||
|
||
- Scope: accessible conversations in the current workspace. Match channel names and resolved DM participant names. Apply the same query to all three sections, preserving section headings and the intentional favorite duplication.
|
||
- Search must not reveal inaccessible private-channel names, cross-workspace favorites, historical message contents, or hidden participant metadata. Search by participant name depends on a safe participant projection; do not ship a field that appears to support it while only matching “Direct message.”
|
||
- The DM creation picker has its own **Find people** search over eligible workspace members. A person match in that picker is not an existing DM result.
|
||
- Initial filters: archive status (**Active**, default; **Archived**; **All**) and channel visibility (**All**, default; **Public**; **Private**, explicitly labeled “Channels only”). The visibility filter applies to nondirect rows in both Channels and Favorites; DMs remain unaffected. Display applied criteria and a Clear filters action.
|
||
- Search and filters intersect. Search does not silently include archives; an empty filtered result offers Clear filters / Show archived. Mark filtered-empty separately from no conversations.
|
||
- Keep all three section headers present, including during a no-results state. Provide one concise result summary counted by unique conversation ID rather than duplicate favorite appearances; section counts, if shown, count visible rows within that section.
|
||
- Do not auto-close the selected conversation or rewrite its URL when filters hide its row. Clearing filters restores the row.
|
||
- No default space grouping. The inspected channel schema has no space link, so **space filter/grouping is deferred**, not simulated from names or parent object IDs. If the architect later justifies optional space metadata, retain the flat default and an explicit unassigned state; do not change channel permissions through a display grouping.
|
||
- Preserve search/filter state while moving between conversations in the same workspace; reset when the workspace/user changes. Persistence across sessions is optional and must be separate from the Spaces sidebar preferences.
|
||
|
||
## 5. Conversation actions and content ownership
|
||
|
||
### Primary-composer editing
|
||
|
||
- Edit acts on an existing message owned by the current user, subject to server authorization; not on the channel name. Preserve the current owner/moderator distinctions rather than widening them (`chat-workspace.tsx:703–719`).
|
||
- Move the selected text into the **primary conversation composer**, visibly labeled “Editing message” with author/time and, for a reply, its thread context. Use **Save changes** and **Cancel** instead of Send. Do not render a second inline edit textarea.
|
||
- Decision for reply edits: use that same primary edit surface with the reply's target/root retained. Suspend new reply submission while editing so two simultaneous composer intentions cannot be confused; thread history may remain visible. This needs explicit state handling because the current thread composer is independent.
|
||
- Preserve the interrupted unsent primary/reply drafts. Save success or Cancel restores them. Switching to another edit target or navigating away with changed edit text must not silently discard it; provide a discard/cancel decision. Access revocation is different: purge sensitive drafts rather than retaining them for later display.
|
||
- A failed save keeps the edited text and offers recovery. Preserve revision/conflict semantics; never retry as an unconditional overwrite. Status text must distinguish saving, saved, failed, and conflict. Do not promise that current composer behavior already supports this.
|
||
|
||
### Message deletion
|
||
|
||
- Delete opens a labeled modal confirmation, with a short message excerpt/context, **Cancel** and **Delete message**. No inline confirmation block, browser confirm, or channel-delete meaning.
|
||
- Explain that the message becomes a deleted placeholder, consistent with `chat-message-list.tsx:105–108`. Do not promise permanent erasure, undo, or deletion of replies without a verified data contract.
|
||
- Initial focus goes to the safe action; focus is contained, Escape cancels when safe, and completion returns focus to the originating message or nearest surviving context. Keep a recoverable error in the dialog if deletion fails; disable duplicate submission while pending.
|
||
- Message action access is the same on hover and keyboard focus, also usable on touch. Identity display and action labels should use meaningful context rather than UUIDs (`chat-message-list.tsx:118` currently includes an ID).
|
||
|
||
## 6. Empty, archived, unavailable, and failure states
|
||
|
||
| State | Required presentation and behavior |
|
||
| --- | --- |
|
||
| Loading navigation/identity | Announce loading; reserve useful space. Do not flash “No conversations” or raw IDs while profile data resolves. |
|
||
| Chat index, conversations exist | “Select a conversation” with navigation available. Creation remains in the relevant section header, not a fourth section. |
|
||
| No Favorites | Explain shortcuts; Add favorite chooses existing conversations. If none exist, direct people to Channels Add or DMs Add without manufacturing defaults. |
|
||
| No Channels | “No channels available.” Offer Create channel when permitted; do not imply no DMs exist. |
|
||
| No DMs | “Start a conversation with someone in this workspace.” DMs Add remains the entry point. |
|
||
| No other eligible people | Picker says no other workspace members are available. Disable Start conversation; no self-DM and no invented invite API. |
|
||
| Search/filter has no matches | “No matching conversations” plus Clear search/filters. Distinct from an empty workspace; preserve the three sections. |
|
||
| Empty accessible conversation | “No messages yet” with the composer if active. A history failure must not appear as empty history. |
|
||
| Archived conversation | Excluded from Active navigation, including active Favorites, but discoverable with Archived/All and by an authorized deep link. Explicit Archived badge/banner; readable history, no send/edit/delete composer actions. Keep the underlying favorite shortcut so it reappears under the appropriate filter. No Restore control without an implemented capability. |
|
||
| Matching direct conversation is already archived | Existing creation logic can return it (`manage-channels.ts:435–448` has no archive exclusion). Open it read-only with clear explanation; do not falsely claim a writable new conversation or silently unarchive it. Product/architecture must confirm whether a later restore capability is needed. |
|
||
| Missing/deleted/inaccessible destination | Generic “Conversation unavailable” without revealing which private resource exists. Purge protected history, participants, drafts, and favorite labels from visible client state. Offer return to Chat; if using the current automatic redirect, carry a safe explanation so it does not look like a random jump. |
|
||
| Stale favorite loses access | Remove the inaccessible target from visible shortcuts and search immediately after denial/refetch. Do not expose old name/avatar as a disabled row. Whether to delete or retain the inaccessible persistence record is a server decision, not permission to display it. |
|
||
| Network/list/favorite failure | Keep failure separate from empty; Retry in place. Do not remove favorites permanently because a temporary fetch failed. A favorite save failure restores its previous state and announces the failure. |
|
||
| User/workspace switch | Reset scoped navigation/selection/draft state and cancel stale results. No names, favorites, or message text from the previous scope may flash in the new one. |
|
||
|
||
Archive is not access loss. Filtering is neither archive nor access loss. These distinctions must survive the extraction of `ChatWorkspace`.
|
||
|
||
## 7. Accessibility and interaction handoff
|
||
|
||
Planning requirements, not verified runtime findings:
|
||
|
||
- One main landmark; named Chat navigation; section headings/disclosure buttons with `aria-expanded` and associated content. Do not nest row action buttons inside navigation links.
|
||
- Hover actions appear on `focus-within`/focus-visible and remain keyboard-operable; touch users have a visible action path. WCAG 2.1.1, 2.4.7, 2.4.11.
|
||
- Active destination, private/archive state, and unread information have text/semantics, not only color. WCAG 1.3.1, 1.4.1, 4.1.2. Duplicate shortcuts must not double-announcement-count overall unread totals.
|
||
- Avatar images are decorative when adjacent text already names the person; use meaningful initials/fallback without replacing the visible name. WCAG 1.1.1.
|
||
- Search results, loading, save/delete completion, and mutation failures have appropriately restrained announcements. WCAG 4.1.3. Do not announce every repeated shortcut as a new event.
|
||
- Disclosure collapse, removal of a favorite, route selection, composer transitions, and dialog close all retain a sensible focus destination. Dialog interaction must not trap focus permanently. WCAG 2.1.2, 2.4.3.
|
||
- At narrow widths/zoom, navigation and thread are alternate reachable views rather than four cramped columns. Verify reflow and unobscured focused controls later (WCAG 1.4.10, 2.4.11).
|
||
|
||
## 8. Handoff decisions and unresolved risks
|
||
|
||
### Decisions to carry into briefs
|
||
|
||
1. Keep the two existing routes and one contextual navigation slot. Do not modify non-Chat navigation behavior.
|
||
2. Favorites are Chat-only shortcuts scoped by user and workspace; duplicates remain in their canonical section. Use a single favorite state per target.
|
||
3. Channels is flat public/private discovery; DMs is participant-oriented. No Defaults, New Message, AI, agent, or space hierarchy.
|
||
4. DMs Add selects 1–99 other human members; include self once in the existing request and open the returned existing/new destination.
|
||
5. Find conversations filters navigation, not message history. Archive is explicit; space/grouping and recent-activity sorting are deferred pending data support.
|
||
6. All editing uses the primary composer with explicit target context and restored drafts; deletion uses a modal with safe focus behavior.
|
||
7. Generic unavailable copy and cache purge take priority over retaining stale identities. Archived-but-authorized content remains discoverable/read-only.
|
||
|
||
### Architecture/product questions that must be resolved, not invented
|
||
|
||
| Risk / gap | Required handoff |
|
||
| --- | --- |
|
||
| Favorites targets are object-only; current listing is user-scoped and its query does not recheck target visibility (`favorites.ts:50–65`). | Architect must choose a channel-capable persistence extension and apply per-channel visibility on reads/writes. Do not send channel IDs as object IDs or rely only on workspace membership for private/direct targets. Preserve existing non-Chat favorite behavior. |
|
||
| Channel listing lacks participant and author identity. Workspace members are not equivalent to conversation participants or historical authors. | Define an authorized minimal profile/participant projection across initial history, pagination, threads, and live/recovered messages. Names/avatar only as appropriate; no global directory or raw-ID fallback. Resolve missing/departed member semantics. |
|
||
| Optional metadata and sorting were mentioned in drafts, but the inspected contract lacks space linkage and activity time. | Do not add space/schema dependencies simply to match draft language. Reassess only if a concrete supported use case requires them. |
|
||
| Existing direct reuse can return an archive; there is no restore procedure in the inspected Chat router. | Confirm read-only reopening is acceptable for this slice, or explicitly scope a restoration feature before offering one. Never create a hidden duplicate for the same set. |
|
||
| Global RightPanel can remain open and show AI content. | Recommended Chat route host policy: suppress unrelated global panel rendering while on Chat, preserving its non-Chat state, and use the Chat thread region only when requested. Confirm with shell owner; this is a Chat-only host change, not removal of global AI navigation. |
|
||
| Extracting navigation can separate queries from the existing revocation/read/recovery lifecycle. | Preserve the authorization/cache-purge seams and channel-ID unread aggregation. Do not mark messages read merely because a row was searched, favorited, or displayed. |
|
||
| Primary composer has local state and two thread/root compose contexts. | Architect must define draft ownership, edit target/revision, cancellation, conflict, and navigation handling before implementation. Do not conflate editing a reply with sending a root message. |
|
||
| Duplicate display names, long group names, many members, and avatar failure have no complete behavior demonstrated here. | Validate identity presentation with representative fixtures and keyboard/screen-reader walkthroughs during implementation. Distinguish unknown from departed only with evidence. |
|
||
|
||
### Later verification scenarios (not executed)
|
||
|
||
Check both Chat routes versus a non-Chat route; the same conversation via Favorites and its canonical section; keyboard/touch disclosure and Add actions; one/many/self-only DM selections; reopening the same group; archived DM reuse; no eligible people; search with hidden selected/archived rows; access loss while an edit or delete dialog is open; draft restoration after cancel/failure; reply edit target retention; user/workspace switching; names missing/colliding; and narrow-screen navigation/thread return focus.
|
||
|
||
This is an independent planning recommendation and a list of implementation dependencies, not a claim that the earlier draft IA/UX/architecture has passed review.
|