// header — session-page header five-piece reflow (S2, 2026-08-17). Scoped // entirely to (max-width: 767px) and appended LAST (after home.css.ts) so // its rules win ties against the shared <=1023px block in layout.css.ts — // same reasoning as home.css.ts: 768-1023px keeps the v1.0.0 drawer header // untouched, >=1024px is a strict no-op. // // DOM this reflows (dsh-client-ui-conversation lib/client.js:6949-7009, // verified live 2026-08-17 at 375px — class names below are the CURRENT // hashed values, always targeted by suffix): // header ConversationSessionHeader root // div.titleRow // div.titleCluster (official: a flex cluster; we flatten it) // nav.crumbs breadcrumb chain, last segment = the title // div.headerActions [data-slot="conversation.session.header.actions"] // div.headerUtilities [data-slot="conversation.session.header.utilities"] // div.headerCorner [data-conversation-header-corner] // [data-slot="conversation.session.header.corner"] // (DSH 0.1.5: the right sidebar's ExpandButton — // hidden below, our workbench button routes to // its controls instead) // div.tabs[role="tablist"] Chat/Trajectory, only when there is >1 view // (DSH <= 0.2.1-rc.2) // div.tabs DSH 0.2.1-rc.3: the row lost its role and // div.viewTabs[role="tablist"][data-conversation-tabs] // div.viewsMoreAnchor "More conversation views" (…) menu launcher, // present while conversation.session.header. // views.actions has registrations (the web app // ships dsh-client-ui-entry-customization) export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) ---------- */ /* New elements render unconditionally (React does not know about media queries); default them to hidden so 768px+ never sees them, then re-enable inside the phone block below. Belt-and-braces alongside the scoped rules — mirrors the existing [data-mobile-nav="*"] desktop no-op list in misc.css.ts. */ [data-mobile-nav="header-back"], [data-mobile-nav="header-viewrow"], [data-mobile-nav="header-activity"], [data-mobile-nav="header-info"], [data-mobile-nav="header-workbench"], /* The workbench close pill's default-hidden used to live in compat.css.ts — but EVERYTHING in layout/compat/misc sits inside one shared (max-width: 1023px) block (opened at the top of layout, closed at the end of misc), so that "top-level" rule silently never applied at >=1024 and the pill rendered as an unstyled button on desktop (2026-08-17 report). This file is appended after the shared block closes, so here it is truly global; the <=767px :has() show rule in compat overrides it with !important. */ [data-mobile-nav="better-sidebar-close"] { display: none; } @media (max-width: 767px) { /* The Files action targeted the dsh-web-ui-all (aionui) explorer sheet — a different, unrelated suite from the workbench entry this slice adds (dsh-better-sidebar). Hidden here only; 768-1023px keeps it exactly as v1.0.0 shipped it. */ [data-mobile-nav="files"] { display: none !important; } /* --- three-column header: [back 92px] [title, truly centered] [utilities 92px] --- */ /* The bottom padding is the view-switch row's seat: that row is position:absolute (see the bottom of this file — it renders two levels deep inside headerActions and can never be a grid item of titleRow), so without a reserve it would hang over the message list. 28px = its height. Unconditional rather than header:has([data-mobile-nav= "header-viewrow"]) on purpose: :has() is silently dropped by pre-105 WebViews (see AGENTS.md) and a 28px overlap on the first message is a worse failure than 28px of empty band in the (never observed in practice) single-view session. Total header chrome: 48 + 28 = 76px. Left/right 8px (S2.1 report leftover, fixed in S4): the official header had zero horizontal padding, so the utilities button cluster's right edge sat flush against the screen edge (x=390 at 390px width, no margin). The 92px grid columns absorb this fine — they're fixed track widths, only the center 1fr column shrinks by 16px. */ [data-phase] header { position: relative; /* Above the scroller's plain and relative-positioned message content, so the ::after fade strip below actually paints over the messages sliding under it (positioned z-auto boxes otherwise win by DOM order — the scroller comes after the header). Deliberately tiny: the seat (7), scroll-to-bottom (8) and every overlay this plugin owns (30+) stay above. */ z-index: 2; padding: 0 8px 28px !important; border-bottom: none !important; } /* No header bottom line — fade instead (real-device round 2, 2026-08-17). The official header in rc.2 drew its border as a \`::after\` 1px bar; in rc.3 (2026-10-25) the host replaced that pseudo-element with a plain 0.5px border-bottom (cleared above) and added a blank 0px header in the hero phase (\`_headerBlank\`). Declaring \`content: ''\` + \`position: absolute; left: 0; right: 0\` explicitly on \`[data-phase="active"] header:not([class$="_headerBlank"])::after\` keeps the 20px fade strip anchored right below the real session header on both rc.2 and rc.3 without touching the scroll container: - When the session header is display:none on the list page, ::after is not generated at all (unlike _scrollBody::before, which stayed in flow at y=0 whenever the header was hidden or blank and showed up at the top of the window on iOS standalone PWA launch before data-view mutated). - :not([class$="_headerBlank"]) excludes the 0-height blank header. - color-mix instead of the transparent keyword: plain \`transparent\` is rgba(0,0,0,0) — WebKit interpolates it through grey. */ [data-phase="active"] header:not([class$="_headerBlank"])::after { content: ''; position: absolute; left: 0; right: 0; top: 100% !important; bottom: auto !important; height: 20px !important; pointer-events: none; background: linear-gradient(to bottom, var(--dsw-alias-bg-base), color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent)) !important; } [data-mobile-nav="frame"]:not([data-mobile-view="session"]) [data-phase] header::after, body:has([data-mobile-nav="home"]:not([data-view="session"])) [data-phase] header::after { content: none !important; display: none !important; } /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for the "标题被挤下去" report). The three items are placed with explicit grid-column but arrive in DOM order crumbs(2) → headerActions(1) → headerUtilities(3): sparse auto-placement never moves its cursor backwards, so headerActions and headerUtilities were pushed onto an implicit SECOND row. Measured at 390px: row1 = crumbs 28px, row2 = back button 44px, titleRow = 72px with the title stuck at the top instead of centered. Pinning the row makes it one 48px band again. */ [data-phase] header [class$="_titleRow"] { position: relative; display: grid; grid-template-columns: 92px 1fr 92px; grid-template-rows: minmax(48px, auto); align-items: center; min-height: 48px; padding: 0 !important; } /* Flatten the official cluster so its two children (crumbs, headerActions) become direct grid items of titleRow instead of a nested flex box. */ [data-phase] header [class$="_titleCluster"] { display: contents; } /* margin-left: 0 cancels layout.css.ts's shared <=1023px "margin-left: auto !important" on this exact selector (it pushed the Files button to the row's right edge in the old non-grid header; inside our grid column it instead shoves the back button 48px right of the column's start — the auto margin absorbs the leftover space between the button's content width and the 92px column). */ [data-phase] header [class$="_headerActions"] { grid-column: 1; grid-row: 1; justify-self: start; margin-left: 0 !important; display: flex; align-items: center; min-width: 0; } /* layout.css.ts's shared <=1023px block hides this element outright (header > :first-child > :last-child { display: none !important }, specificity (0,3,1) — v1.0.0 relocated the desktop Session-log capsule to the drawer footer on every narrow width). That selector is repeated here at equal specificity so the later source order wins the display property; the class-suffix selector alone (0,2,1) would silently lose. */ [data-phase] header [class$="_headerUtilities"], [data-phase] header > :first-child > :last-child { grid-column: 3; grid-row: 1; justify-self: end; display: flex !important; align-items: center; gap: 4px; min-width: 0; } /* justify-self: stretch (the grid default) so this fills the whole 1fr column as a definite-width box — center/center-content happens inside it. justify-self: center (tried first) sizes the item to its content with no automatic column cap, and the obvious fix — max-width: 100% — resolves against the WRONG box here (measured 93.6px at 390px, where the column is ~206px): the item's DOM parent (titleCluster) is display:contents, and percentage max-width does not reliably resolve through a boxless ancestor to the grid track. Stretching sidesteps the percentage entirely — but layout.css.ts's shared <=1023px block ALSO caps this exact selector at max-width: 24vw !important (93.6px at 390px — the value this rule used to silently inherit once its own max-width was dropped in favor of stretch), so that must be reset to none here too. */ [data-phase] header [class$="_crumbs"] { grid-column: 2; grid-row: 1; justify-self: stretch; max-width: none !important; display: flex; align-items: center; justify-content: center; min-width: 0; overflow: hidden; } /* Subagent sessions keep ONE ancestor crumb (2026-08-18): the PC header lets you tap the parent session's title to switch back — that affordance was lost on phone when the whole parent chain was hidden as "多余项". Show only the immediate parent (for agent-teams member sessions that IS the captain/main session; deeper ancestors stay hidden — a 3-level chain cannot fit the ~206px center column at 390px). The parent crumb is the official breadcrumb