feat(app): match the session column to the design and use pretty text wrapping (#54020)

This commit is contained in:
Luke Parker
2026-10-09 18:40:52 +10:00
committed by GitHub
Unverified
parent 1aae67e853
commit f9e892dc85
12 changed files with 38 additions and 11 deletions
@@ -215,6 +215,7 @@ for (const direction of ["ltr", "rtl"] as const) {
)
const composer = page.locator('[data-component="session-composer-dock"] > div')
const timeline = page.locator('[data-slot="session-timeline-scroll"]')
const summary = page.getByRole("dialog", { name: "Session details", exact: true })
await expect(row).toBeInViewport()
const before = { row: (await row.boundingBox())!, composer: (await composer.boundingBox())! }
@@ -229,12 +230,13 @@ for (const direction of ["ltr", "rtl"] as const) {
return Math.max(Math.abs(message.x - before.row.x), Math.abs(input.x - before.composer.x))
})
.toBeLessThan(1)
// The capped session column can end before the panel, so the overlay is measured against the whole timeline.
await expect
.poll(async () => {
const message = (await row.boundingBox())!
const view = (await timeline.boundingBox())!
const details = (await summary.boundingBox())!
return Math.min(message.x + message.width, details.x + details.width) - Math.max(message.x, details.x)
return Math.min(view.x + view.width, details.x + details.width) - Math.max(view.x, details.x)
})
.toBeGreaterThan(0)
const text = summary.getByText(branch, { exact: true })
+2 -2
View File
@@ -7,7 +7,7 @@ import { createAnimatedPresence } from "@/runtime/animated-presence"
export function ComposerDropzone(props: {
active: boolean
input?: { image?: boolean; pdf?: boolean }
identity?: () => unknown
identity?: () => string | undefined
}) {
const language = useLanguage()
const [elements, setElements] = createStore<{ dropzone?: HTMLDivElement }>({})
@@ -52,7 +52,7 @@ export function ComposerDropzone(props: {
>
<div class="absolute inset-0 bg-v2-background-bg-base/25" />
<div
class="absolute inset-y-0 left-1/2 w-full -translate-x-1/2 md:max-w-200 2xl:max-w-[1000px]"
class="absolute inset-y-0 left-1/2 w-full -translate-x-1/2 md:max-w-session"
style={{
"-webkit-mask-image": "linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%)",
"mask-image": "linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%)",
@@ -31,7 +31,7 @@ export function SessionComposerRegion(props: {
<div
classList={{
"w-full px-3 pointer-events-auto": true,
"md:max-w-[1000px] md:mx-auto": controller.centered(),
"md:max-w-session md:mx-auto": controller.centered(),
}}
>
<Show when={controller.state.websearch.request()}>
@@ -47,7 +47,7 @@ export function PreparingComposer(props: { pending: PendingSession }) {
return (
<div data-component="session-composer-dock" class="w-full shrink-0 bg-v2-background-bg-base pb-3">
<div class="mx-auto w-full max-w-[1000px] px-3">
<div class="mx-auto w-full px-3 md:max-w-session">
<ComposerEditor controller={editor} modelControlsVisible={false} />
</div>
</div>
+1 -1
View File
@@ -66,7 +66,7 @@ function PreparingSession(props: { sessionID: string; pending: PendingSession })
>
<div data-component="session-preparing" data-workspace-session class="min-h-0 flex-1 overflow-y-auto">
<SessionIdentityHeader sessionID={props.sessionID} />
<div class="mx-auto w-full min-w-0 max-w-[1000px] px-4 pb-5 md:px-5">
<div class="mx-auto w-full min-w-0 px-4 pb-5 md:max-w-session md:px-6">
<SessionUserMessage
sessionID={props.sessionID}
message={props.pending.message}
@@ -160,7 +160,7 @@ function MessageTimelineView(
if (!directory) return
void data.location.vcs.sync({ directory }).catch(() => undefined)
})
const turnPadding = () => "px-4 md:px-5"
const turnPadding = () => "px-4 md:px-6"
const showHeader = createMemo(() => !props.hideHeader && (props.data.showHeader() || workspaceSession()))
const pinned = createMemo(() => props.pinned)
const messageByID = projection.messageByID
@@ -410,7 +410,7 @@ function MessageTimelineView(
<div
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-[1000px] md:mx-auto": props.centered,
"md:max-w-session md:mx-auto": props.centered,
}}
>
<div
@@ -41,6 +41,11 @@
margin-bottom: 0;
}
/* Pretty wrapping would move settled words as each token arrives, so the growing block stays greedy until it completes. */
&[data-streaming] > [data-markdown-block]:last-child {
text-wrap-style: auto;
}
/* Headings: Sized by level, distinguished by color and spacing */
h1 {
font-size: 17px;
@@ -817,6 +817,7 @@ export function Markdown(
return (
<div
data-component="markdown"
data-streaming={local.streaming ? "" : undefined}
dir="auto"
classList={{
...local.classList,
+2
View File
@@ -22,6 +22,8 @@ const unsafeCSS = `
color-mix(in lab, var(--diffs-bg) 98%, var(--diffs-mixer)),
color-mix(in lab, var(--diffs-bg) 95%, var(--diffs-mixer))
);
/* The app's code wrapping rules do not reach this shadow tree, and pretty is inherited from the page. */
text-wrap-style: auto;
}
[data-diff],
@@ -140,7 +140,7 @@ export function createSessionTimelineRowRenderer(input: {
: undefined
}
const padding = () => input.padding?.() ?? "px-4 md:px-5"
const padding = () => input.padding?.() ?? "px-4 md:px-6"
const indexGroupContents = (refs: PartRef[]) => {
const result = new Map<string, Map<string, SessionMessageAssistant["content"][number]>>()
@@ -470,7 +470,7 @@ export function createSessionTimelineRowRenderer(input: {
data-timeline-spacing={Predicate.isTagged(props.row, "AssistantPart") ? props.row.spacing : undefined}
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-[1000px] md:mx-auto": input.centered?.(),
"md:max-w-session md:mx-auto": input.centered?.(),
"pt-2": Predicate.isTagged(props.row, "AssistantPart") && props.row.spacing === "tool",
"pt-4": Predicate.isTagged(props.row, "AssistantPart") && props.row.spacing === "content",
"pt-6": Predicate.isTagged(props.row, "Error"),
+14
View File
@@ -23,6 +23,7 @@
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS.
8. Avoid one-word last lines in all wrapped text. Chromium keeps the line count, so heights do not change.
*/
html,
@@ -34,6 +35,7 @@ html,
font-feature-settings: var(--font-family-sans--font-feature-settings, normal); /* 5 */
font-variation-settings: var(--font-family-sans--font-variation-settings, normal); /* 6 */
-webkit-tap-highlight-color: transparent; /* 7 */
text-wrap-style: pretty; /* 8 */
}
/*
@@ -111,6 +113,7 @@ strong {
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
5. Keep code wrapping greedy so wrapped lines break where an editor would.
*/
code,
@@ -121,6 +124,17 @@ pre {
font-feature-settings: var(--font-family-mono--font-feature-settings, normal); /* 2 */
font-variation-settings: var(--font-family-mono--font-variation-settings, normal); /* 3 */
font-size: 1em; /* 4 */
text-wrap-style: auto; /* 5 */
}
/*
Keep earlier lines still while the user types.
*/
input,
textarea,
[contenteditable] {
text-wrap-style: stable;
}
/*
@@ -49,6 +49,9 @@
--container-5xl: 64rem;
--container-6xl: 72rem;
--container-7xl: 80rem;
/* Shared by the session timeline, composer dock, and drop target so their edges stay aligned.
The composer dock's 12px gutter leaves the design's 720px composer. */
--container-session: 744px;
--font-sans: var(--font-family-sans);
--font-sans--font-feature-settings: var(--font-family-sans--font-feature-settings);