Files
fusion/packages/dashboard/app/components/ChatView.css
gsxdsm 6777eea5d2 FN-7631: add content search to Chat sidebar with title-only toggle
Chat sidebar search now matches message content by default, not just the conversation title/agent, with an opt-out toggle to restore title-only filtering.

- Add ChatStore.searchSessionsByMessageContent (parameterized LIKE ... ESCAPE) for server-side content search across sessions
- GET /chat/sessions route (register-chat-routes.ts, legacy.ts) gains q/titleOnly query params, debounced server-side content lookup merged with local title/agent matches
- useChat hook exposes searchInTitleOnly state and wires debounced content search into session list results
- ChatView renders a "Search in title only" toggle beside the search box (desktop + mobile) and shows a "Matched: ..." preview snippet on content-matched rows
- Task-planner sessions remain excluded from content matches via the same common-feed visibility guard used for the normal session list
- Add unit/integration tests: chat-store content-search, chat-routes API test, ChatView content-search test
- Update docs/dashboard-guide.md to document the new content search behavior and toggle
- Add changeset fn-7631-chat-content-search.md (@runfusion/fusion minor)

Files changed:
 .changeset/fn-7631-chat-content-search.md          |   7 +
 docs/dashboard-guide.md                            |   2 +
 .../__tests__/chat-store.content-search.test.ts    | 157 +++++++++++++++++++++
 packages/core/src/chat-store.ts                    |  64 +++++++++
 packages/core/src/chat-types.ts                    |   8 ++
 packages/dashboard/app/api/legacy.ts               |  23 ++-
 packages/dashboard/app/components/ChatView.css     |  30 ++++
 packages/dashboard/app/components/ChatView.tsx     |  26 ++++
 .../__tests__/ChatView.autosize.test.tsx           |   2 +
 .../__tests__/ChatView.content-search.test.tsx     | 114 +++++++++++++++
 .../components/__tests__/ChatView.draft.test.tsx   |   2 +
 .../__tests__/ChatView.hash-mention.test.tsx       |   2 +
 .../__tests__/ChatView.mobile-render.test.tsx      |   2 +
 .../components/__tests__/ChatView.rooms.test.tsx   |   2 +
 .../__tests__/ChatView.scroll-to-top.test.tsx      |   2 +
 .../components/__tests__/ChatView.test-harness.tsx |   2 +
 packages/dashboard/app/hooks/useChat.ts            | 105 ++++++++++++--
 .../dashboard/src/__tests__/chat-routes.test.ts    |  78 ++++++++++
 .../dashboard/src/routes/register-chat-routes.ts   |  39 ++++-
 packages/i18n/locales/en/app.json                  |   2 +
 packages/i18n/locales/es/app.json                  |   2 +
 packages/i18n/locales/fr/app.json                  |   2 +
 packages/i18n/locales/ko/app.json                  |   2 +
 packages/i18n/locales/zh-CN/app.json               |   2 +
 packages/i18n/locales/zh-TW/app.json               |   2 +
 25 files changed, 667 insertions(+), 12 deletions(-)

Fusion-Task-Id: FN-7631

Fusion-Task-Lineage: bc68b489-26a7-453e-901b-bda816af364e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-07 22:06:17 -07:00

2459 lines
58 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ── Chat View ─────────────────────────────────────────────────────────────── */
.chat-view {
container: chat-view / inline-size;
display: flex;
flex-direction: column;
flex: 1 1 auto;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.chat-view__body {
display: flex;
flex: 1 1 auto;
min-width: 0;
min-height: 0;
overflow: hidden;
}
/* Sidebar */
.chat-sidebar {
min-width: 0;
max-width: 500px; /* FN-6210: defensive cap — matches CHAT_SIDEBAR_MAX_WIDTH */
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
background: var(--bg-secondary);
}
.chat-sidebar--hidden {
display: none;
}
.chat-sidebar-resize-handle {
position: relative;
width: var(--space-sm);
flex-shrink: 0;
cursor: col-resize;
background: transparent;
touch-action: none;
transition: background var(--transition-fast);
}
.chat-sidebar-resize-handle::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: var(--space-xs);
transform: translateX(-50%);
}
.chat-sidebar-resize-handle:hover::before,
.chat-sidebar-resize-handle:active::before {
background: color-mix(in srgb, var(--todo) 30%, transparent);
}
.chat-sidebar-resize-handle:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-sidebar-scope-toggle {
display: flex;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
}
/*
FNXC:ChatHeader 2026-06-22-16:18:
Direct/Rooms now lives in the Chat ViewHeader immediately before New Chat. The control must scale with available header width: bounded flex-basis, minmax grid columns, and truncating labels let it fit desktop, narrow pop-out, and mobile headers without forcing the title/actions to overlap.
FNXC:ChatHeader 2026-06-22-18:44:
Keep the Direct/Rooms segmented control height-aligned with ViewHeader's action row and collapse labels to icons when Chat is very narrow. Buttons use height:100% inside the padded track so they cannot grow taller than the background.
FNXC:ChatHeader 2026-06-22-20:28:
When the movable chat popup is resized narrow, collapse Direct/Rooms labels to icon-only from the ChatView container width so the Chat title remains visible even on a wide desktop viewport.
*/
.chat-view-header-scope-toggle {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
flex: 1 1 clamp(128px, 24vw, 220px);
width: clamp(128px, 24vw, 220px);
min-width: min(128px, 100%);
max-width: 220px;
height: var(--view-header-content-row, 28px);
box-sizing: border-box;
padding: 2px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
}
.chat-sidebar-scope-btn {
flex: 1;
padding: var(--space-sm) var(--space-md);
border: none;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-muted);
font-size: var(--space-md);
transition: background var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-xs);
min-width: 0;
min-height: 0;
height: 100%;
padding: 0 clamp(var(--space-xs), 1.2vw, var(--space-sm));
border: 1px solid transparent;
border-radius: calc(var(--radius-md) - 2px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn svg {
flex: 0 0 auto;
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-sidebar-scope-btn:hover {
background: var(--card-hover);
color: var(--text);
}
.chat-sidebar-scope-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-sidebar-scope-btn--active {
background: var(--card);
color: var(--text);
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn--active {
border-color: var(--todo);
}
.chat-sidebar-rooms {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.chat-sidebar-rooms-header {
padding: var(--space-sm) var(--space-md);
}
.chat-sidebar-rooms-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-muted);
padding: var(--space-xl) var(--space-lg);
font-size: var(--space-md);
}
.chat-room-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
border: none;
border-radius: var(--radius-md);
background: transparent;
color: var(--text);
padding: var(--space-sm) var(--space-md);
text-align: left;
cursor: pointer;
}
.chat-room-item:hover {
background: var(--card-hover);
}
.chat-room-item:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-room-item--active {
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
.chat-room-item-details {
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.chat-room-item-name-row {
display: inline-flex;
align-items: center;
}
.chat-room-item-name {
font-weight: 500;
}
.chat-unread-dot {
display: inline-block;
width: var(--space-sm);
height: var(--space-sm);
border-radius: 50%;
background: var(--color-info);
flex-shrink: 0;
margin-inline-start: var(--space-xs);
}
.chat-room-item-meta {
color: var(--text-muted);
font-size: var(--space-sm);
}
.chat-room-item-delete {
color: var(--text-muted);
}
.chat-room-item-delete:hover,
.chat-room-item-delete:focus-visible {
color: var(--color-error);
}
.chat-room-thread-header {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-md) var(--space-lg);
border-bottom: 1px solid var(--border);
}
.chat-room-thread-members {
margin-left: auto;
display: flex;
gap: var(--space-xs);
overflow-x: auto;
}
.chat-room-empty-pane {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
padding: var(--space-2xl);
}
.chat-sidebar-search-container {
padding: var(--space-sm);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
/*
FNXC:ChatSearch 2026-07-07-00:00:
Compact toggle button, reusing .btn/.btn-sm so it inherits the shared button visual language
instead of one-off styling. aria-pressed drives the active/inactive look via existing token
colors (no hardcoded hex).
*/
.chat-search-title-only-toggle {
align-self: flex-start;
font-size: 11px;
color: var(--text-dim);
}
.chat-search-title-only-toggle[aria-pressed="true"] {
color: var(--text);
border-color: var(--accent);
background: var(--surface-hover);
}
.chat-session-preview--matched {
color: var(--text-dim);
font-size: 11px;
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-sidebar-search-wrapper {
position: relative;
display: flex;
align-items: center;
}
.chat-sidebar-search-icon {
position: absolute;
left: var(--space-md);
color: var(--text-dim);
pointer-events: none;
}
.chat-sidebar-search {
width: 100%;
padding: var(--space-sm) var(--space-md) var(--space-sm) calc(var(--space-2xl) + var(--space-xs));
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg);
color: var(--text);
font-size: 13px;
}
.chat-session-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 4px 8px;
}
.chat-empty-state {
color: var(--text-muted);
font-size: 13px;
}
.chat-empty-state--padded {
padding: var(--space-md);
}
.chat-sidebar-list > .chat-empty-state--padded {
flex: 0 0 auto;
display: block;
text-align: left;
}
.chat-session-item {
padding: 10px 12px;
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
flex-direction: column;
gap: 2px;
transition: background 0.15s;
position: relative;
}
.chat-session-item:hover {
background: var(--card-hover);
}
.chat-session-item--active {
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
.chat-session-item--active:hover {
background: color-mix(in srgb, var(--todo) 16%, transparent);
}
.chat-session-title {
font-weight: 500;
font-size: 14px;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: calc((var(--space-md) * 4) + (var(--space-xs) * 2));
}
.chat-session-preview {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: calc((var(--space-md) * 4) + (var(--space-xs) * 2));
}
.chat-session-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: var(--text-dim);
}
.chat-session-meta-model {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-session-meta-model .provider-icon {
flex-shrink: 0;
}
/*
FNXC:ChatSidebar 2026-07-02-00:00:
Direct conversation rows expose edit and delete as a compact action pair. Keep the pair outside the row text flow so title/preview truncation stays stable across desktop, mobile, and compact right-dock layouts.
*/
.chat-session-actions {
position: absolute;
top: var(--space-sm);
right: var(--space-sm);
display: inline-flex;
align-items: center;
gap: var(--space-xs);
}
.chat-session-action-btn {
width: calc(var(--space-md) * 2);
min-width: calc(var(--space-md) * 2);
height: calc(var(--space-md) * 2);
min-height: calc(var(--space-md) * 2);
flex: 0 0 auto;
border-radius: var(--radius-sm);
color: var(--text-muted);
}
.chat-session-action-btn:hover,
.chat-session-action-btn:focus-visible {
color: var(--text);
}
.chat-session-delete-btn:hover,
.chat-session-delete-btn:focus-visible {
color: var(--color-error);
}
/* Context menu for session items */
.chat-session-context-menu {
position: fixed;
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
padding: var(--space-xs);
z-index: 100;
}
.chat-session-context-menu button {
display: flex;
align-items: center;
gap: var(--space-sm);
width: 100%;
padding: var(--space-sm) var(--space-md);
border: none;
background: none;
color: var(--text);
cursor: pointer;
border-radius: var(--radius-sm);
font-size: 13px;
}
.chat-session-context-menu button:hover {
background: var(--card-hover);
}
/* Main chat thread */
.chat-thread {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
position: relative;
}
.chat-thread-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 8px;
}
/*
FNXC:Chat 2026-06-22-15:30:
The active-chat pane header must give the title the full available line to the LEFT of the actions. The identity row grows (flex:1, min-width:0) and the title truncates with an ellipsis instead of being squeezed to one-word-per-line by the model badge, eye/preview toggle, and New Chat button. Those action controls stay flex-shrink:0 so they keep their size and the title absorbs all slack.
*/
.chat-thread-header-identity {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
flex: 1 1 auto;
min-width: 0;
}
.chat-thread-header-title {
flex: 1 1 auto;
font-weight: 600;
font-size: 15px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Model badge keeps its intrinsic size next to the truncating title. */
.chat-thread-header-identity .chat-model-tag {
flex-shrink: 0;
}
/*
FNXC:ChatContextWindow 2026-06-27-00:00:
The Direct-chat context budget indicator is a secondary desktop/tablet header affordance. Keep it badge-like and shrinkable so the thread title and render toggle retain priority, and hide it at mobile widths where the session switcher owns the header.
*/
.chat-thread-header-context {
flex: 0 1 auto;
min-width: 0;
max-width: calc(var(--space-xl) * 5);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--font-size-xs);
padding: calc(var(--space-xs) / 4) calc(var(--space-sm) - (var(--space-xs) / 2));
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text-muted);
}
.chat-mobile-session-menu {
position: relative;
min-width: 0;
max-width: 100%;
}
.chat-room-switcher-menu {
position: relative;
min-width: 0;
}
.chat-room-switcher-trigger {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
padding: 0;
font: inherit;
line-height: normal;
cursor: pointer;
}
.chat-room-switcher-trigger:hover {
color: var(--text);
background: var(--card-hover);
}
.chat-room-switcher-trigger:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-room-switcher-trigger > svg {
color: var(--text-muted);
flex-shrink: 0;
}
.chat-room-switcher-dropdown {
position: absolute;
top: calc(100% + var(--space-xs));
left: 0;
min-width: calc(var(--space-xl) * 6);
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: var(--space-xs);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
box-shadow: var(--shadow-lg);
z-index: 4;
max-height: calc(var(--space-xl) * 10);
overflow-y: auto;
}
.chat-room-switcher-option {
width: 100%;
display: flex;
align-items: center;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
cursor: pointer;
text-align: left;
padding: var(--space-sm) var(--space-md);
line-height: normal;
}
.chat-room-switcher-option:hover {
background: var(--card-hover);
}
.chat-room-switcher-option:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-room-switcher-option--active {
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
.chat-mobile-session-trigger {
width: 100%;
max-width: 100%;
min-width: 0;
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: var(--space-sm);
border: none;
border-radius: var(--radius-md);
background: transparent;
color: var(--text);
padding: var(--space-xs) var(--space-sm);
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
font: inherit;
line-height: normal;
text-align: left;
}
.chat-mobile-session-trigger .chat-thread-header-title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-mobile-session-trigger .provider-icon,
.chat-mobile-session-trigger > svg:first-child {
flex-shrink: 0;
}
.chat-mobile-session-trigger > svg {
width: var(--icon-size-md);
height: var(--icon-size-md);
}
.chat-mobile-session-trigger svg:last-child {
margin-left: auto;
color: var(--text-muted);
flex-shrink: 0;
}
.chat-mobile-session-trigger:hover {
background: var(--card-hover);
}
.chat-mobile-session-trigger:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-mobile-session-dropdown {
position: absolute;
top: calc(100% + var(--space-xs));
left: 0;
right: 0;
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: var(--space-xs);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
box-shadow: var(--shadow-lg);
z-index: 4;
max-height: calc(var(--space-xl) * 10);
overflow-y: auto;
}
/*
FNXC:Chat 2026-06-16-22:12:
Mobile chat session switching needs a dedicated rename tap target beside each session without nesting buttons, so the row owns layout while the title and rename controls remain independently keyboard accessible.
*/
.chat-mobile-session-option-row {
display: flex;
align-items: stretch;
gap: var(--space-xs);
border-radius: var(--radius-sm);
}
.chat-mobile-session-option {
width: 100%;
display: flex;
align-items: flex-start;
min-height: calc(var(--space-lg) * 2.25);
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
cursor: pointer;
text-align: left;
padding: var(--space-sm) var(--space-md);
line-height: normal;
}
.chat-mobile-session-rename {
flex-shrink: 0;
align-self: stretch;
color: var(--text-muted);
}
.chat-mobile-session-rename:hover {
color: var(--text);
background: var(--card-hover);
}
.chat-mobile-session-option:hover {
background: var(--card-hover);
}
.chat-mobile-session-option:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-mobile-session-option--active {
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
.chat-mobile-session-new {
display: flex;
align-items: center;
gap: var(--space-sm);
width: 100%;
margin-top: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border: none;
border-top: 1px solid var(--border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
cursor: pointer;
text-align: left;
line-height: normal;
}
.chat-mobile-session-new svg {
flex-shrink: 0;
color: var(--text-muted);
}
.chat-mobile-session-new:hover {
background: var(--card-hover);
}
.chat-mobile-session-new:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-mobile-session-option-title {
width: 100%;
display: block;
line-height: normal;
white-space: normal;
overflow-wrap: anywhere;
}
.chat-view-header-new-chat {
flex: 0 1 auto;
min-width: fit-content;
}
.chat-view-header-icon {
flex: 0 0 auto;
}
.chat-view .view-header__actions {
min-width: 0;
}
/*
FNXC:ChatHeader 2026-07-02-17:26:
Mobile direct-chat detail uses ViewHeader as the only visible header row. The back arrow must be the far-left visible/focusable control and the session selector must stay on that same non-wrapping row, so hide the entire Chat title/icon shell from layout while retaining the accessible heading and make the actions cluster own the row from the left edge.
*/
.chat-view--mobile-direct-thread > .view-header {
flex-wrap: nowrap;
}
.chat-view--mobile-direct-thread > .view-header .view-header__title {
position: absolute;
inline-size: var(--btn-border-width);
block-size: var(--btn-border-width);
margin: calc(var(--btn-border-width) * -1);
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.chat-view--mobile-direct-thread > .view-header .view-header__actions {
flex: 1 1 auto;
width: 100%;
min-width: 0;
margin-left: 0;
flex-wrap: nowrap;
justify-content: flex-start;
}
.chat-view--mobile-direct-thread .chat-back-btn {
flex: 0 0 auto;
}
.chat-view--mobile-direct-thread .chat-mobile-session-menu {
flex: 1 1 0;
min-width: 0;
}
@media (max-width: 768px), (max-height: 480px) {
.chat-view-header-scope-toggle {
flex-basis: clamp(112px, 42vw, 180px);
width: clamp(112px, 42vw, 180px);
max-width: 180px;
}
}
@container chat-view (max-width: 560px) {
.chat-view-header-scope-toggle {
flex: 0 0 72px;
width: 72px;
min-width: 72px;
max-width: 72px;
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn {
padding: 0;
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn span {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
}
@media (max-width: 460px) {
.chat-view-header-scope-toggle {
flex: 0 0 72px;
width: 72px;
min-width: 72px;
max-width: 72px;
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn {
padding: 0;
}
.chat-view-header-scope-toggle .chat-sidebar-scope-btn span {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
}
/*
FNXC:ChatModal 2026-06-22-13:22:
The old Quick Chat panel is replaced by the full ChatView inside a movable FloatingWindow. In floating mode ChatView's shared header is the only visible modal header and doubles as the drag handle, with minimize/close controls in the same action row.
*/
.chat-view--floating .view-header {
cursor: grab;
user-select: none;
touch-action: none;
}
.chat-view--floating .view-header:active {
cursor: grabbing;
}
/*
FNXC:ChatModal 2026-06-22-14:38:
Floating Chat can become narrow while the browser viewport remains desktop-sized. Mirror the mobile one-pane layout with a class driven by the modal ResizeObserver so a narrow pop-out hides the sidebar and shows the mobile thread/list surfaces.
*/
.chat-view--narrow .chat-view__body {
flex-direction: column;
}
.chat-view--narrow .chat-sidebar {
width: 100%;
min-width: 100%;
max-width: 100%;
height: 100%;
max-height: none;
border-right: none;
border-bottom: 1px solid var(--border);
flex-direction: column;
}
.chat-view--narrow .chat-sidebar-header {
display: none;
}
.chat-view--narrow .chat-sidebar-search {
min-height: calc(var(--space-2xl) + var(--space-xs));
}
.chat-view--narrow .chat-sidebar-list {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.chat-view--narrow .chat-sidebar:not(.chat-sidebar--hidden) + .chat-thread {
display: none;
}
/*
FNXC:ChatModal 2026-06-22-18:00:
In the narrow/mobile chat layout there is no room for an expand/maximize affordance in the header. Hide only the expand controls (main Chat pop-out and floating-modal maximize); keep minimize/close visible so the user can still dismiss or dock the chat.
*/
.chat-view--narrow [data-testid="chat-pop-out"],
.chat-view--narrow [data-testid="chat-modal-maximize"] {
display: none;
}
@media (max-width: 768px) {
.chat-view [data-testid="chat-pop-out"],
.chat-view [data-testid="chat-modal-maximize"] {
display: none;
}
}
.chat-rename-label {
display: block;
margin-bottom: var(--space-xs);
color: var(--text-muted);
font-size: 0.875rem;
}
.chat-rename-input {
width: 100%;
margin-bottom: var(--space-md);
}
@media (max-width: 768px) {
.chat-mobile-session-option-row {
align-items: stretch;
}
.chat-mobile-session-rename {
min-width: calc(var(--space-lg) * 2.25);
}
}
/*
FNXC:ChatRenderToggle 2026-07-04-00:00:
The thread-wide Markdown/plain render toggle (`.chat-thread-header-render-toggle*`, desktop + mobile floating variants) was removed per FN-7541. Chat always renders Markdown now; the button and its CSS shells no longer exist.
*/
/* Messages */
.chat-messages {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: var(--space-lg);
display: flex;
flex-direction: column;
gap: var(--space-md);
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
/* Stop iOS rubber-band scroll from propagating to the parent /
document when the user swipes on the edge of the messages list
with the keyboard up. Without this, the bounce escapes the
scroll container, the page itself shifts, and the chat-thread
transform momentarily exposes the body background as a gray
box partially covering the messages. */
overscroll-behavior: contain;
}
.chat-messages > .chat-empty-state {
flex: 1 1 auto;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-md);
padding: var(--space-xl) var(--space-lg);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--surface);
text-align: center;
box-sizing: border-box;
}
.chat-message {
max-width: 75%;
padding: 10px 14px;
border-radius: 12px;
font-size: 14px;
line-height: 1.5;
word-break: break-word;
}
.chat-jump-to-latest {
position: absolute;
left: 50%;
bottom: calc(var(--space-xl) * 3);
transform: translateX(-50%);
z-index: 2;
}
.chat-message--user {
align-self: flex-end;
background: var(--accent);
color: var(--accent-text);
border-bottom-right-radius: var(--radius-sm);
}
.chat-message--assistant {
align-self: flex-start;
background: var(--surface-1);
color: var(--text);
border-bottom-left-radius: var(--radius-sm);
}
.chat-message--failure {
background: var(--status-error-bg);
border: var(--btn-border-width) solid var(--status-error-bg-deep);
}
/*
FNXC:ChatView 2026-06-23-11:40:
Tablet Chat View has enough message-pane width for assistant prose, markdown, tool output, failure details, and attachments, so agent-side bubbles need a wider reading measure than the shared desktop/user cap. Use the ChatView container width instead of the viewport so floating Quick Chat and other narrow hosts keep their bounded bubble layout.
*/
@container chat-view (min-width: 48.0625rem) and (max-width: 64rem) {
.chat-message--assistant,
.chat-message--streaming,
.chat-message--failure {
max-width: 88%;
}
}
/*
FNXC:ChatView 2026-06-28-14:47:
Narrow chat hosts need full-width bubbles for prose, code, tool output, failures, streams, and attachments in already-constrained columns. Key this to the ChatView container width, not the viewport, so Quick Chat popups and the right dock on desktop viewports match phone-width Chat View behavior.
*/
@container chat-view (max-width: 30rem) {
.chat-message {
max-width: 100%;
}
}
.chat-message-avatar {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-size: 12px;
color: var(--text-muted);
}
/* Model-only chats hide the per-message agent identity. The toggle still
* needs a place to live, so collapse the avatar row to just the toggle on
* the right edge instead of a full identity strip. */
.chat-message-avatar.chat-message-avatar--toolbar-only {
margin-bottom: 0;
min-height: 0;
justify-content: flex-end;
}
.chat-message-render-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: auto;
width: calc(var(--space-md) * 3);
height: calc(var(--space-md) * 3);
min-width: calc(var(--space-md) * 3);
min-height: calc(var(--space-md) * 3);
padding: 0;
border: none;
color: var(--text-muted);
background: transparent;
cursor: pointer;
outline: none;
}
.chat-message--assistant .chat-message-render-toggle {
opacity: 0.6;
transition: opacity var(--transition-fast);
}
.chat-message--assistant:hover .chat-message-render-toggle,
.chat-message-render-toggle:focus-visible,
.chat-message-render-toggle.chat-message-render-toggle--plain {
opacity: 1;
}
.chat-message-render-toggle:hover {
color: var(--text);
background: color-mix(in srgb, var(--surface) 55%, transparent);
}
.chat-message-render-toggle:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-message-render-toggle--plain {
color: var(--text);
}
.chat-model-tag {
font-size: 11px;
padding: 1px 6px;
border-radius: 4px;
background: var(--surface);
color: var(--text-muted);
margin-left: 6px;
white-space: nowrap;
vertical-align: middle;
}
.chat-message-content {
white-space: pre-wrap;
}
.chat-message-content--plain {
white-space: pre-wrap;
}
.chat-message-content--markdown {
white-space: normal;
overflow-wrap: anywhere;
}
.chat-message-content--failure {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.chat-message-failure-summary-row {
display: flex;
align-items: center;
gap: var(--space-sm);
color: var(--color-error);
font-weight: 600;
}
.chat-message-failure-label {
min-width: 0;
}
.chat-message-failure-summary {
color: var(--text);
white-space: pre-wrap;
}
.chat-message-failure-badges {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
}
.chat-message-failure-badge {
display: inline-flex;
align-items: center;
min-height: calc(var(--space-lg) * 1.5);
padding: 0 var(--space-sm);
border-radius: var(--radius-pill);
background: var(--status-error-bg-deep);
color: var(--color-error);
font-family: var(--font-mono);
font-size: calc(var(--space-sm) + var(--space-xs));
}
.chat-message-failure-details {
border: var(--btn-border-width) solid var(--status-error-bg-deep);
border-radius: var(--radius-md);
background: var(--status-error-bg-deep);
}
.chat-message-failure-details summary {
display: flex;
align-items: center;
gap: var(--space-sm);
cursor: pointer;
list-style: none;
padding: var(--space-sm) var(--space-md);
color: var(--text-muted);
}
.chat-message-failure-details summary::-webkit-details-marker {
display: none;
}
.chat-message-failure-details summary:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-message-failure-detail {
margin: 0;
padding: 0 var(--space-md) var(--space-md);
color: var(--text);
font-family: var(--font-mono);
white-space: pre-wrap;
}
.chat-message-failure-reference {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
align-items: center;
padding: 0 var(--space-md) var(--space-md);
color: var(--text-muted);
}
.chat-message-failure-reference-label {
font-weight: 600;
}
.chat-message-failure-reference-value {
min-width: 0;
overflow-wrap: anywhere;
}
.chat-message-failure-reference-link {
margin-left: auto;
border-color: var(--status-error-bg-deep);
background: var(--status-error-bg-deep);
color: var(--color-error);
text-decoration: none;
}
.chat-message-failure-reference-link:hover,
.chat-message-failure-reference-link:focus-visible {
border-color: var(--color-error);
background: var(--status-error-bg);
color: var(--color-error);
}
.chat-message-failure-reference-details {
margin-left: auto;
}
.chat-message-failure-reference-details summary {
list-style: none;
}
.chat-message-failure-reference-details summary::-webkit-details-marker {
display: none;
}
.chat-message-failure-reference-meta {
display: grid;
gap: var(--space-sm);
margin: var(--space-sm) 0 0;
padding: var(--space-sm);
border: var(--btn-border-width) solid var(--status-error-bg-deep);
border-radius: var(--radius-md);
background: var(--status-error-bg);
}
.chat-message-failure-reference-meta div {
display: grid;
gap: var(--space-xs);
}
.chat-message-failure-reference-meta dt {
color: var(--text-muted);
font-size: calc(var(--space-sm) + var(--space-xs));
font-weight: 600;
}
.chat-message-failure-reference-meta dd {
margin: 0;
color: var(--text);
font-family: var(--font-mono);
overflow-wrap: anywhere;
}
.chat-message-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-xs);
margin-top: var(--space-xs);
}
.chat-message-copy-action,
.chat-message-scroll-to-top-action {
display: flex;
align-items: center;
justify-content: center;
margin-top: var(--space-xs);
margin-left: auto;
width: calc(var(--space-lg) * 2);
height: calc(var(--space-lg) * 2);
min-width: calc(var(--space-lg) * 2);
min-height: calc(var(--space-lg) * 2);
padding: 0;
border: none;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--surface) 35%, transparent);
color: var(--text-muted);
cursor: pointer;
opacity: 0.85;
transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast);
}
.chat-message-actions .chat-message-copy-action,
.chat-message-actions .chat-message-scroll-to-top-action {
margin-top: 0;
margin-left: 0;
}
.chat-message-copy-action:hover,
.chat-message-scroll-to-top-action:hover {
opacity: 1;
color: var(--text);
background: color-mix(in srgb, var(--surface) 55%, transparent);
}
.chat-message-copy-action:focus-visible,
.chat-message-scroll-to-top-action:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
opacity: 1;
}
.chat-message-copy-action--success {
color: var(--color-success);
}
.chat-message-copy-action--error {
color: var(--color-error);
}
/*
FNXC:ChatMessageEdit 2026-07-07-09:00:
User messages in direct (model-loop) chat can carry an inline edit affordance mirroring the
assistant copy/scroll-to-top action row pattern above, plus an inline textarea editor. Editing
an earlier message resumes the conversation from that point, forgetting everything after it, so
keep the affordance visually consistent with existing chat-message-actions styling rather than a
one-off treatment. Rendered only for user messages on direct/model-loop sessions (never rooms,
CLI-agent sessions, or while streaming) — see StandardChatSurface.tsx `showEditAction`.
*/
.chat-message-actions--user {
justify-content: flex-start;
}
.chat-message-edit-action {
display: flex;
align-items: center;
justify-content: center;
width: calc(var(--space-lg) * 2);
height: calc(var(--space-lg) * 2);
min-width: calc(var(--space-lg) * 2);
min-height: calc(var(--space-lg) * 2);
padding: 0;
border: none;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--accent-text) 15%, transparent);
color: var(--accent-text);
cursor: pointer;
opacity: 0.85;
transition: opacity var(--transition-fast), background var(--transition-fast);
}
.chat-message-edit-action:hover {
opacity: 1;
background: color-mix(in srgb, var(--accent-text) 25%, transparent);
}
.chat-message-edit-action:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
opacity: 1;
}
.chat-message-edit-editor {
display: flex;
flex-direction: column;
gap: var(--space-xs);
width: 100%;
}
.chat-message-edit-textarea {
width: 100%;
resize: vertical;
min-height: calc(var(--space-lg) * 4);
font: inherit;
color: var(--text);
background: var(--surface);
}
.chat-message-edit-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-xs);
}
@media (max-width: 768px) {
.chat-message-edit-action {
width: calc(var(--space-lg) * 2.25);
height: calc(var(--space-lg) * 2.25);
min-width: calc(var(--space-lg) * 2.25);
min-height: calc(var(--space-lg) * 2.25);
}
.chat-message-edit-textarea {
min-height: calc(var(--space-lg) * 5);
}
}
.chat-message-content--markdown > :first-child {
margin-top: 0;
}
.chat-message-content--markdown > :last-child {
margin-bottom: 0;
}
.chat-message-content--markdown p,
.chat-message-content--markdown ul,
.chat-message-content--markdown ol,
.chat-message-content--markdown blockquote,
.chat-message-content--markdown pre,
.chat-message-content--markdown table {
margin: 0 0 var(--space-sm);
}
.chat-message-content--markdown ul,
.chat-message-content--markdown ol {
padding-left: var(--space-lg);
}
.chat-message-content--markdown code {
font-family: var(--font-mono);
font-size: 0.8125rem;
}
.chat-message-content--markdown :not(pre) > code {
padding: 0 var(--space-xs);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--surface) 55%, transparent);
}
.chat-markdown-pre {
margin: 0 0 var(--space-sm);
padding: var(--space-sm);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--surface) 65%, transparent);
overflow-x: auto;
white-space: pre;
max-width: 100%;
}
.chat-markdown-table {
display: block;
width: 100%;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.chat-markdown-table th,
.chat-markdown-table td {
border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
padding: var(--space-xs) var(--space-sm);
}
.chat-message-time {
font-size: 11px;
color: var(--text-dim);
margin-top: 4px;
}
.chat-message-thinking {
margin-top: 6px;
}
.chat-message-thinking summary {
font-size: 12px;
color: var(--text-muted);
cursor: pointer;
}
.chat-message-thinking-content {
font-size: 13px;
color: var(--text-muted);
padding: 8px;
background: var(--bg);
border-radius: 6px;
margin-top: 4px;
white-space: pre-wrap;
font-family: var(--font-mono, monospace);
overflow-x: auto;
}
/* === Chat Tool Calls === */
.chat-tool-calls {
margin-top: var(--space-sm);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.chat-tool-calls-header {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
color: var(--text-muted);
font-size: var(--space-md);
}
.chat-tool-calls-header-icon {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--space-sm);
block-size: var(--space-sm);
color: var(--text-muted);
line-height: 1;
}
.chat-tool-calls-group {
border: var(--btn-border-width, 1px) solid color-mix(in srgb, var(--border) 85%, transparent);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--surface) 35%, transparent);
}
.chat-tool-calls-group-summary {
display: flex;
align-items: center;
gap: var(--space-xs);
list-style: none;
cursor: pointer;
padding: var(--space-xs);
color: var(--text-muted);
border-radius: var(--radius-sm);
font-size: var(--space-md);
min-width: 0;
}
.chat-tool-calls-group-summary::marker {
content: "";
}
.chat-tool-calls-group-summary::-webkit-details-marker {
display: none;
}
.chat-tool-calls-group-summary:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-tool-calls-count {
white-space: nowrap;
flex-shrink: 0;
}
.chat-tool-calls-names {
color: var(--text-dim);
font-family: var(--font-mono, monospace);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
min-width: 0;
font-size: 0.6875rem;
}
.chat-tool-calls-group-status {
margin-left: auto;
font-size: 0.6875rem;
color: var(--text-dim);
flex-shrink: 0;
}
.chat-tool-calls-group > .chat-tool-call {
margin: 0 var(--space-xs) var(--space-xs);
}
.chat-tool-calls-group > .chat-tool-call:last-child {
margin-bottom: var(--space-xs);
}
.chat-tool-call {
border: var(--btn-border-width, 1px) solid color-mix(in srgb, var(--border) 85%, transparent);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--surface) 35%, transparent);
}
.chat-tool-call summary {
display: flex;
align-items: center;
gap: var(--space-xs);
list-style: none;
cursor: pointer;
padding: var(--space-xs);
color: var(--text-muted);
border-radius: var(--radius-sm);
font-size: var(--space-md);
min-width: 0;
}
.chat-tool-call summary::marker {
content: "";
}
.chat-tool-call summary::-webkit-details-marker {
display: none;
}
.chat-tool-call summary:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-tool-call-status-dot {
width: calc((var(--space-xs) + var(--space-sm)) / 2);
height: calc((var(--space-xs) + var(--space-sm)) / 2);
border-radius: 50%;
background: var(--color-success);
flex-shrink: 0;
}
.chat-tool-call-name {
font-family: var(--font-mono, monospace);
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-tool-call-preview {
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
font-size: 0.6875rem;
}
.chat-tool-call-status-text {
margin-left: auto;
font-size: 0.6875rem;
text-transform: lowercase;
}
.chat-tool-call-content {
margin: var(--space-xs);
padding: var(--space-sm);
border-radius: var(--radius-sm);
background: var(--bg);
font-family: var(--font-mono, monospace);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.chat-tool-call-row {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-xs);
align-items: start;
}
.chat-tool-call-label {
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.6875rem;
}
.chat-tool-call-value {
color: var(--text);
word-break: break-word;
white-space: pre-wrap;
}
.chat-tool-call--running .chat-tool-call-status-dot {
background: var(--color-info);
animation: tool-call-pulse var(--transition-slow) infinite;
}
.chat-tool-call--error summary {
color: var(--color-error);
}
.chat-tool-call--error .chat-tool-call-status-dot {
background: var(--color-error);
}
.chat-tool-call-row--error {
background: color-mix(in srgb, var(--color-error) 10%, transparent);
border-radius: var(--radius-sm);
padding: var(--space-xs);
}
.chat-tool-calls--compact .chat-tool-calls-header,
.chat-tool-calls--compact .chat-tool-calls-group-summary,
.chat-tool-calls--compact .chat-tool-calls-names,
.chat-tool-calls--compact .chat-tool-calls-group-status,
.chat-tool-calls--compact .chat-tool-call summary,
.chat-tool-calls--compact .chat-tool-call-content,
.chat-tool-calls--compact .chat-tool-call-preview,
.chat-tool-calls--compact .chat-tool-call-value,
.chat-tool-calls-group--compact .chat-tool-calls-group-summary,
.chat-tool-calls-group--compact .chat-tool-calls-names {
font-size: 0.6875rem;
}
@keyframes tool-call-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* Streaming indicator */
.chat-message--streaming {
align-self: flex-start;
background: var(--surface-1);
color: var(--text);
border-bottom-left-radius: 4px;
opacity: 0.9;
}
.chat-typing-indicator {
display: inline-flex;
gap: 4px;
padding: 4px 0;
}
.chat-typing-indicator span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-muted);
animation: chat-typing-bounce 1.4s infinite ease-in-out;
}
.chat-typing-indicator span:nth-child(1) { animation-delay: 0s; }
.chat-typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.chat-typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes chat-typing-bounce {
0%, 80%, 100% { opacity: 0.3; }
40% { opacity: 1; }
}
/* Chat waiting state */
.chat-message-content--waiting {
color: var(--text-muted);
font-style: italic;
font-size: 13px;
}
/* Input area */
.chat-input-area {
position: relative;
padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: var(--space-sm);
/* Stop iOS rubber-band escape when the user swipes on the composer
edge with the keyboard up. Without it the page momentarily shifts
and the body background peeks through above the messages. */
overscroll-behavior: contain;
}
.chat-input-row {
--chat-input-control-size: calc(var(--space-lg) * 2.5);
display: flex;
align-items: flex-end;
gap: var(--space-sm);
width: 100%;
}
/* === Chat Skill Menu === */
.chat-skill-menu {
position: absolute;
left: var(--space-lg);
bottom: calc(100% + var(--space-xs));
min-width: calc((var(--space-xl) * 10) + var(--space-2xl) + (var(--space-xs) * 2));
max-width: calc(100% - (var(--space-lg) * 2));
max-height: calc(var(--space-xl) * 10);
overflow-y: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: 50;
}
.chat-skill-menu-item {
width: 100%;
border: none;
background: transparent;
color: inherit;
text-align: left;
padding: var(--space-sm) var(--space-md);
cursor: pointer;
display: flex;
flex-direction: column;
gap: var(--space-xs);
transition: background var(--transition-fast);
}
.chat-skill-menu-item:hover,
.chat-skill-menu-item--highlighted {
background: var(--card-hover);
}
.chat-skill-menu-item:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
background: var(--card-hover);
}
.chat-skill-menu-item-name {
font-size: 0.8125rem;
color: var(--text);
font-family: var(--font-mono);
}
.chat-skill-menu-item-description {
font-size: 0.75rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc((var(--space-xl) * 10) + var(--space-lg) + var(--space-xs));
}
.chat-skill-menu-empty {
padding: var(--space-sm) var(--space-md);
color: var(--text-muted);
font-size: 0.8125rem;
}
.chat-input-textarea {
/* FN-5322: this textarea sits directly inside `.chat-input-wrapper`, which
is a column flex container (via AgentMentionPopup.css). Keep it out of
vertical flex sizing so the inline autosize height from ChatView.tsx is
honored instead of collapsing back toward the one-line minimum. */
flex: 0 0 auto;
width: 100%;
box-sizing: border-box;
-webkit-appearance: none;
appearance: none;
resize: none;
overflow-y: hidden;
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px 14px;
background: var(--bg);
color: var(--text);
font-size: 14px;
font-family: inherit;
line-height: 1.4;
max-height: 640px;
min-height: 40px;
/* Mobile keeps the desktop 640px cap because the surrounding chat-input-area
and thread flex bounds already prevent the composer from overtaking
the message pane before textarea scrolling engages. */
/* touch-action: manipulation allows tap-to-focus (so iOS doesn't
auto-dismiss the keyboard between gesture frames) and blocks
double-tap zoom + pan/zoom gestures. We previously tried `none`
to lock the composer against pan, but that broke first-tap
focus on iOS. */
touch-action: manipulation;
overscroll-behavior: contain;
}
/* FN-6212: On tablet viewports (769–1024px), cap the composer to 200px
so it doesn't consume most of the visible thread. Desktop keeps 640px;
mobile is already flex-constrained by the chat-thread layout. */
@media (min-width: 769px) and (max-width: 1024px) {
.chat-input-textarea {
max-height: 200px;
}
}
.chat-input-textarea:focus {
outline: none;
border-color: var(--accent);
}
.chat-input-send {
width: var(--chat-input-control-size);
min-height: var(--chat-input-control-size);
border-radius: 50%;
border: none;
background: var(--accent);
color: var(--accent-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.chat-input-send:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.chat-input-stop {
width: var(--chat-input-control-size);
min-height: var(--chat-input-control-size);
border-radius: var(--radius-md);
border: none;
background: color-mix(in srgb, var(--color-error) 15%, transparent);
color: var(--color-error);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background var(--transition-fast), transform var(--transition-fast), color var(--transition-fast);
}
.chat-input-stop-icon {
display: block;
inline-size: var(--space-sm);
block-size: var(--space-sm);
border-radius: var(--radius-xs);
background: currentColor;
}
.chat-input-stop:hover {
background: color-mix(in srgb, var(--color-error) 25%, transparent);
}
.chat-input-stop:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-input-stop:active {
transform: scale(0.97);
}
.chat-attach-btn {
min-width: calc(var(--space-lg) * 2);
min-height: calc(var(--space-lg) * 2);
align-self: flex-end;
}
.chat-input-wrapper {
flex: 1;
}
.chat-input-wrapper--dragover {
border: 1px dashed var(--todo);
border-radius: var(--radius-md);
}
.chat-attachment-previews {
display: flex;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
overflow-x: auto;
border-top: 1px solid var(--border);
background: var(--surface);
width: 100%;
}
.chat-attachment-preview {
width: calc(var(--space-xl) * 3);
height: calc(var(--space-xl) * 3);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
position: relative;
flex-shrink: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.chat-attachment-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.chat-attachment-preview-name {
font-size: 0.625rem;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-muted);
padding: var(--space-xs);
}
.chat-attachment-remove {
position: absolute;
top: calc(var(--space-xs) * -1);
right: calc(var(--space-xs) * -1);
background: color-mix(in srgb, var(--color-error) 80%, transparent);
color: var(--text);
border: none;
border-radius: var(--radius-pill);
width: calc(var(--space-md) * 2);
height: calc(var(--space-md) * 2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background var(--transition-fast), transform var(--transition-fast);
}
.chat-attachment-remove:hover {
background: color-mix(in srgb, var(--color-error) 90%, transparent);
}
.chat-attachment-remove:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.chat-attachment-remove:active {
transform: scale(0.97);
}
.chat-message-attachments {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
margin-top: var(--space-xs);
}
.chat-message-attachment-link {
display: inline-flex;
border-radius: var(--radius-sm);
}
.chat-message-attachment {
max-width: calc(var(--space-xl) * 12);
border-radius: var(--radius-sm);
}
.chat-message-attachment-file {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--surface) 50%, transparent);
font-size: 0.75rem;
color: var(--text-muted);
text-decoration: none;
}
.chat-message-attachment-file:hover {
background: color-mix(in srgb, var(--surface) 70%, transparent);
}
.chat-message-attachment-file:focus-visible,
.chat-message-attachment-link:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
/* === Chat Pending Message === */
/*
FNXC:ChatComposer 2026-06-27-00:00:
Queued-message banners stack above the composer input with a capped scroll area, and the shared divider below them makes the queued state visually distinct without rendering an empty rule when the FIFO queue is empty.
*/
.chat-pending-stack {
display: flex;
flex-direction: column;
gap: var(--space-xs);
width: 100%;
max-height: calc(var(--space-xl) * 6);
overflow-y: auto;
}
.chat-pending-message {
display: flex;
align-items: center;
gap: var(--space-sm);
width: 100%;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--todo) 10%, transparent);
color: var(--text-muted);
font-size: 0.75rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.chat-pending-divider {
width: 100%;
border-top: 1px solid var(--border);
}
.chat-pending-message span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-pending-message-dismiss {
background: none;
border: none;
color: var(--text-dim);
cursor: pointer;
padding: var(--space-xs);
line-height: 1;
flex-shrink: 0;
font-size: 0.875rem;
}
.chat-pending-message-dismiss:hover {
color: var(--text-muted);
}
/* Sidebar footer contains the primary chat-scope action on desktop and mobile. */
.chat-sidebar-footer {
display: block;
padding: var(--space-sm) var(--space-md);
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
.chat-sidebar-footer .chat-sidebar-footer-btn {
width: 100%;
justify-content: center;
min-height: 40px;
}
/* === Mobile: one pane at a time =========================================
Sidebar (session list) and thread are siblings of .chat-view. On mobile
we show whichever one is "active" full-width: when the sidebar is
visible, the thread is hidden; when the sidebar is hidden (a session is
open), the sidebar is collapsed via .chat-sidebar--hidden and the
thread takes the full viewport. The thread already renders a back
button (ChevronLeft) on mobile to flip back to the session list. */
@media (max-width: 768px) {
.chat-pending-stack {
max-height: calc(var(--space-xl) * 5);
}
.chat-pending-message {
align-items: flex-start;
}
.chat-view__body {
flex-direction: column;
}
.chat-sidebar {
width: 100%;
min-width: 100%;
max-width: 100%; /* FN-6210: mobile sidebar spans full viewport */
height: 100%;
max-height: none;
border-right: none;
border-bottom: 1px solid var(--border);
flex-direction: column;
}
.chat-sidebar-header {
display: none;
}
.chat-sidebar-search {
min-height: calc(var(--space-2xl) + var(--space-xs));
}
.chat-sidebar-list {
flex: 1;
min-height: 0;
overflow-y: auto;
}
/* When the sidebar is shown, the thread is hidden so only the session
list is visible. When the sidebar is hidden, the thread fills the
viewport. Adjacent-sibling selector keeps logic in CSS only. */
.chat-sidebar:not(.chat-sidebar--hidden) + .chat-thread {
display: none;
}
.chat-thread--keyboard-active {
height: calc(var(--vv-height, calc(100dvh - var(--keyboard-overlap, 0px))) - var(--header-height));
max-height: calc(var(--vv-height, calc(100dvh - var(--keyboard-overlap, 0px))) - var(--header-height));
/* NOTE: the translateY drift compensation is applied imperatively in
JS (see ChatView's vv `apply()`), NOT here. Declaring
`transform`/`will-change: transform` in CSS keeps a non-`none`
transform on .chat-thread for the entire keyboard-active window —
and since .chat-thread is an ancestor of the focused composer
textarea, iOS Safari treats establishing that containing block as a
reason to blur the input and collapse the keyboard the instant it
opens. JS only sets a transform when there is real viewport drift
(offsetTop > 0); at the focus moment offsetTop is 0, so the
ancestor stays `transform: none` and the keyboard stays up. */
}
.chat-thread--keyboard-active .chat-input-area {
padding-bottom: calc(var(--space-md) + env(safe-area-inset-bottom, 0px) + var(--chat-keyboard-accessory-clearance, 0px));
}
/* On mobile, the active scope affordance uses a full-width pinned footer. */
.chat-sidebar-footer {
display: block;
padding: var(--space-sm) var(--space-md);
padding-bottom: max(var(--space-sm), env(safe-area-inset-bottom, 0));
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
.chat-sidebar-footer .chat-sidebar-footer-btn {
width: 100%;
justify-content: center;
min-height: 40px;
}
.chat-thread-header {
flex-wrap: nowrap;
}
.chat-messages {
min-height: 0;
overflow-y: auto;
padding: var(--space-lg) var(--space-sm);
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
}
.chat-messages > .chat-empty-state {
padding: var(--space-xl) var(--space-sm);
border: none;
border-radius: 0;
background: transparent;
box-sizing: border-box;
}
.chat-thread-header-identity {
flex: 1 1 auto;
min-width: 0;
white-space: nowrap;
overflow: visible;
}
.chat-mobile-session-menu {
width: 100%;
}
.chat-room-switcher-dropdown {
left: 0;
right: auto;
min-width: calc(var(--space-xl) * 5);
max-width: min(calc(var(--space-xl) * 10), calc(100vw - var(--space-2xl)));
}
.chat-thread-header-identity .chat-thread-header-title,
.chat-thread-header-identity .chat-model-tag {
overflow: hidden;
text-overflow: ellipsis;
}
.chat-thread-header-context {
display: none;
}
.chat-sidebar-scope-btn {
min-height: calc(var(--space-lg) * 2.25);
}
/* FN-4352/FN-4385/FN-7441: direct session row action pair stays visible and text keeps clearance for both edit and delete touch targets. */
.chat-session-title,
.chat-session-preview {
padding-right: calc((var(--space-md) * 4) + (var(--space-xs) * 2));
}
.chat-message--assistant .chat-message-render-toggle {
opacity: 1;
}
/* FN-4352: secondary hover-revealed row action keeps base compact sizing (inflated by FN-3628). */
.chat-message-copy-action {
opacity: 1;
}
.chat-message-scroll-to-top-action {
opacity: 1;
}
.chat-tool-calls-group-summary,
.chat-tool-call summary {
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
}
.chat-tool-calls-names,
.chat-tool-call-name,
.chat-tool-call-status-text,
.chat-tool-calls-group-status,
.chat-tool-calls-count {
white-space: nowrap;
}
.chat-tool-calls-group-status,
.chat-tool-call-status-text {
margin-left: auto;
}
/* primary touch targets per AGENTS.md convention */
.chat-attach-btn {
min-width: calc(var(--space-lg) * 2.25);
min-height: calc(var(--space-lg) * 2.25);
}
.chat-input-send,
.chat-input-stop {
min-width: var(--chat-input-control-size);
min-height: var(--chat-input-control-size);
}
.chat-attachment-preview {
width: calc(var(--space-xl) * 2.5);
height: calc(var(--space-xl) * 2.5);
}
.chat-attachment-remove {
width: calc(var(--space-lg) * 2.25);
height: calc(var(--space-lg) * 2.25);
}
.chat-message {
max-width: 100%;
}
.chat-message-failure-details summary,
.chat-message-failure-reference {
align-items: flex-start;
flex-wrap: wrap;
}
.chat-message-failure-reference-link {
margin-left: 0;
}
.chat-new-dialog {
width: 95vw;
margin: var(--space-lg);
}
.chat-thread-header {
padding: var(--space-sm) var(--space-md);
}
.chat-skill-menu {
left: var(--space-md);
right: calc(var(--space-md) + var(--mobile-nav-height));
min-width: 0;
max-width: none;
}
.chat-jump-to-latest {
bottom: calc(var(--space-xl) * 4);
}
.chat-sidebar-rooms-header .btn {
width: 100%;
justify-content: center;
}
.chat-room-thread-header {
padding: var(--space-sm) var(--space-md);
}
.chat-room-thread-header .btn-icon {
min-height: 36px;
min-width: 36px;
}
.chat-room-item {
padding: var(--space-sm);
}
.chat-room-item-delete {
flex-shrink: 0;
}
.chat-room-thread-members {
max-width: 50%;
}
}
.chat-mention-chip {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
margin: 0 var(--space-xs);
background: color-mix(in srgb, var(--todo) 14%, transparent);
color: var(--todo);
border: var(--btn-border-width, 1px) solid color-mix(in srgb, var(--todo) 30%, transparent);
border-radius: var(--radius-pill);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.5);
font-weight: 500;
white-space: nowrap;
}
.chat-mention-chip--non-member {
background: color-mix(in srgb, var(--color-warning) 12%, transparent);
color: var(--text-muted);
border-color: color-mix(in srgb, var(--color-warning) 35%, transparent);
}
.chat-message--user .chat-mention-chip {
color: var(--accent-text);
background: color-mix(in srgb, var(--accent-text) 18%, transparent);
border-color: color-mix(in srgb, var(--accent-text) 40%, transparent);
}
.chat-message--user .chat-mention-chip--non-member {
background: color-mix(in srgb, var(--color-warning) 30%, transparent);
color: var(--accent-text);
border-color: color-mix(in srgb, var(--color-warning) 55%, transparent);
}