From f270b03dba797bb09815f47cb5e36bce3359dd25 Mon Sep 17 00:00:00 2001
From: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Date: Tue, 20 Oct 2026 14:09:43 +0200
Subject: [PATCH 1/3] fix(session-ui): refine completion notice styling
---
packages/session-ui/src/timeline/session-timeline-row.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/session-ui/src/timeline/session-timeline-row.tsx b/packages/session-ui/src/timeline/session-timeline-row.tsx
index 27fcc6a19eb..a54b946997f 100644
--- a/packages/session-ui/src/timeline/session-timeline-row.tsx
+++ b/packages/session-ui/src/timeline/session-timeline-row.tsx
@@ -633,7 +633,7 @@ export function createSessionTimelineRowRenderer(input: {
)}
@@ -1064,7 +1064,7 @@ function outcomeNoticeIcon(outcome: Outcome) {
if (outcome === "cancelled") return "circle-ban-sign"
- return "circle-check"
+ return "check"
}
function outcomeNoticeColor(outcome: Outcome | undefined) {
@@ -1074,7 +1074,7 @@ function outcomeNoticeColor(outcome: Outcome | undefined) {
if (outcome === "cancelled") return "text-v2-state-fg-warning"
- return "text-v2-state-fg-info"
+ return "text-v2-text-text-code-accent"
}
function outcomeNoticeKey(state: string | undefined) {
From fb917cc9c6b74eb693674000f2a090ffc0c3cb67 Mon Sep 17 00:00:00 2001
From: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Date: Tue, 20 Oct 2026 14:13:45 +0200
Subject: [PATCH 2/3] fix(app): stack session breadcrumbs in narrow headers
---
.../regression/patch-sticky-header.spec.ts | 3 +-
.../app/e2e/regression/session-header.spec.ts | 31 ++
.../subagent-child-navigation.spec.ts | 4 +-
.../src/session/session-identity-header.css | 88 ++++++
.../src/session/session-identity-header.tsx | 60 ++--
.../src/session/timeline/message-timeline.tsx | 292 +++++++++---------
.../app/src/session/timeline/virtualizer.tsx | 2 +-
7 files changed, 309 insertions(+), 171 deletions(-)
diff --git a/packages/app/e2e/regression/patch-sticky-header.spec.ts b/packages/app/e2e/regression/patch-sticky-header.spec.ts
index c4548e0b40c..e302b5a2217 100644
--- a/packages/app/e2e/regression/patch-sticky-header.spec.ts
+++ b/packages/app/e2e/regression/patch-sticky-header.spec.ts
@@ -148,6 +148,7 @@ const scenarios = [
for (const scenario of scenarios) {
for (const [width, direction] of [
[1400, "ltr"],
+ [900, "ltr"],
[390, "rtl"],
] as const) {
test(`${scenario.name}: file headers stay flush at ${width}px in ${direction}`, async ({ page }, info) => {
@@ -176,7 +177,7 @@ for (const scenario of scenarios) {
const scroller = page.locator('[data-slot="session-timeline-scroll"] .scroll-view__viewport')
const toolTitle = scroller.locator('[data-slot="collapsible-trigger"][data-locked]')
await expect(toolTitle).toHaveCount(scenario.title ? 1 : 0)
- await expect(scroller.locator("[data-session-title]")).toHaveCount(width === 1400 ? 1 : 0)
+ await expect(scroller.locator("[data-session-title]")).toHaveCount(width >= 768 ? 1 : 0)
for (const file of scenario.files) {
const name = new RegExp(`${file}\\.ts`)
diff --git a/packages/app/e2e/regression/session-header.spec.ts b/packages/app/e2e/regression/session-header.spec.ts
index 31b4c2d36e0..09b320be9ae 100644
--- a/packages/app/e2e/regression/session-header.spec.ts
+++ b/packages/app/e2e/regression/session-header.spec.ts
@@ -9,6 +9,7 @@ const tabs = (page: Page) => page.locator('[data-slot="titlebar-tabs"] a')
for (const direction of ["ltr", "rtl"] as const) {
test(`session header groups controls and exposes session details in ${direction}`, async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 })
await mockStressTimeline(page)
await seed(page, {
tabs: [fixture.sourceID, fixture.targetID],
@@ -65,6 +66,36 @@ for (const direction of ["ltr", "rtl"] as const) {
await header.getByRole("textbox").press("Escape")
await expect(header.getByRole("heading")).toHaveText(fixture.expected.targetTitle)
+ for (const width of [900, 1440]) {
+ await page.setViewportSize({ width, height: 900 })
+ await expect
+ .poll(async () => {
+ const [location, title, menu, summary, sidebar] = await Promise.all(
+ [project, header.getByRole("heading"), more, details, review].map((control) => control.boundingBox()),
+ )
+
+ if (!location || !title || !menu || !summary || !sidebar) return false
+ const gap = direction === "ltr" ? menu.x - title.x - title.width : title.x - menu.x - menu.width
+
+ const aligned =
+ Math.abs(menu.y + menu.height / 2 - summary.y - summary.height / 2) <= 0.5 &&
+ Math.abs(menu.y + menu.height / 2 - sidebar.y - sidebar.height / 2) <= 0.5
+
+ return (
+ Math.abs(gap - 2) <= 0.5 &&
+ aligned &&
+ (width === 900
+ ? Math.abs(location.y - menu.y - menu.height - 8) <= 0.5
+ : Math.abs(location.y + location.height / 2 - menu.y - menu.height / 2) <= 0.5)
+ )
+ })
+ .toBe(true)
+
+ const screenshot = test.info().outputPath(`header-${width}.png`)
+ await page.screenshot({ path: screenshot })
+ await test.info().attach(`header-${width}`, { path: screenshot, contentType: "image/png" })
+ }
+
await review.click()
await expect(review).toHaveAttribute("aria-expanded", "true")
await expect(page.locator("#review-panel")).toBeVisible()
diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts
index 49183fc78fd..29e1aa2701f 100644
--- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts
+++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts
@@ -153,14 +153,14 @@ test("keeps the active nested session visible in a narrow desktop header", async
const separator = header.locator('[data-slot="session-title-ancestors"] + [data-slot="session-title-separator"]')
await expect(header.locator('[data-slot="session-title-ancestor"]')).toHaveText([parentTitle, childTitle])
await expect(heading).toBeInViewport()
- await expect(separator).toBeInViewport()
+ await expect(separator).toBeHidden()
const bounds = await heading.boundingBox()
expect(bounds).not.toBeNull()
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(820)
await page.evaluate(() => document.documentElement.setAttribute("dir", "rtl"))
await expect(heading).toBeInViewport()
- await expect(separator).toBeInViewport()
+ await expect(separator).toBeHidden()
const root = header.locator(`[data-slot="session-title-ancestor"][data-session-id="${parentID}"]`)
await root.focus()
diff --git a/packages/app/src/session/session-identity-header.css b/packages/app/src/session/session-identity-header.css
index 51ec5bec854..ceec2591670 100644
--- a/packages/app/src/session/session-identity-header.css
+++ b/packages/app/src/session/session-identity-header.css
@@ -1,3 +1,91 @@
.session-options-menu [data-component="menu-v2-item"] {
padding-inline-end: 30px;
}
+
+.session-header-layout {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ inline-size: 100%;
+ block-size: 48px;
+}
+
+.session-header-location {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ min-inline-size: 0;
+ max-inline-size: 60%;
+}
+
+.session-header-location > [data-slot="session-location-trigger"] {
+ max-inline-size: 100%;
+}
+
+.session-header-title {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ flex: 1;
+ min-inline-size: 0;
+}
+
+.session-header-title > [data-slot="session-title-child"] {
+ min-inline-size: 0;
+ max-inline-size: 100%;
+ flex-shrink: 1;
+}
+
+.session-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ margin-inline-start: 6px;
+}
+
+.session-header-fade {
+ top: 48px;
+}
+
+@container (max-width: 1000px) {
+ .session-header-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 8px;
+ block-size: 80px;
+ padding-block: 10px;
+ }
+
+ .session-header-title {
+ grid-column: 1;
+ grid-row: 1;
+ block-size: 28px;
+ }
+
+ .session-header-actions {
+ grid-column: 2;
+ grid-row: 1;
+ margin-inline-start: 0;
+ }
+
+ .session-header-location {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ max-inline-size: 100%;
+ block-size: 24px;
+ }
+
+ .session-header-location > [data-slot="session-title-separator"]:last-child,
+ .session-header-location > div > [data-slot="session-title-separator"] {
+ display: none;
+ }
+
+ .session-header-fade {
+ top: 80px;
+ }
+
+ [data-slot="session-timeline-scroll"] {
+ --session-header-height: 80px;
+ }
+}
diff --git a/packages/app/src/session/session-identity-header.tsx b/packages/app/src/session/session-identity-header.tsx
index 241b7276579..b4be1cf1953 100644
--- a/packages/app/src/session/session-identity-header.tsx
+++ b/packages/app/src/session/session-identity-header.tsx
@@ -8,7 +8,7 @@ import {
ProjectAvatar,
} from "@opencode/ui/project-avatar"
import { useNavigate } from "@solidjs/router"
-import { createMemo, For, Show, type ComponentProps, type ParentProps } from "solid-js"
+import { createMemo, For, Show, type ComponentProps, type JSX, type ParentProps } from "solid-js"
import { containsDirectory, getFilename } from "@opencode/util/path"
import { useServer } from "@/runtime/server/current"
import { ServerConnection, useServers } from "@/runtime/server/registry"
@@ -42,12 +42,24 @@ export function SessionTitleHeader(props: ParentProps) {
{/* Stuck timeline headers stack above this fade, so it only fades content scrolling under the bar. */}
>
)
}
+export function SessionHeaderLayout(props: ParentProps<{ location: JSX.Element; actions?: JSX.Element }>) {
+ return (
+
+ )
+}
+
/**
* Where the session runs: the project and its worktree. Opens a card with the server, the full path and the location's
* actions.
@@ -163,10 +175,7 @@ export function SessionLocation(props: {
{(name) => (
<>
-
+
/
@@ -432,9 +441,9 @@ export function SessionIdentityHeader(props: { sessionID: string; session?: Sess
return (
-
-
-
+
)}
-
-
- {(value) => (
-
- {value()}
-
- )}
-
-
-
-
+ >
+ }
+ >
+
+
+ {(value) => (
+
+ {value()}
+
+ )}
+
+
)
diff --git a/packages/app/src/session/timeline/message-timeline.tsx b/packages/app/src/session/timeline/message-timeline.tsx
index 8791fe26e87..20d89099626 100644
--- a/packages/app/src/session/timeline/message-timeline.tsx
+++ b/packages/app/src/session/timeline/message-timeline.tsx
@@ -25,7 +25,12 @@ import { isWorkspaceDirectory } from "@/workspaces/paths"
import { parseCommentNote, readPromptPresentation } from "@/composer/comment-note"
import { parseVoiceDelegation } from "./voice-delegation"
import { useCommand } from "@/shell/commands/command"
-import { SessionAncestorTrail, SessionLocation, SessionTitleHeader } from "../session-identity-header"
+import {
+ SessionAncestorTrail,
+ SessionHeaderLayout,
+ SessionLocation,
+ SessionTitleHeader,
+} from "../session-identity-header"
import { SessionHeaderSpacer } from "@/session/header/session-header"
import { SessionWorkingIndicator } from "@/session/header/session-working-indicator"
@@ -451,9 +456,9 @@ function MessageTimelineView(
header={
-
-
-
+
)}
-
- {(value) => (
- <>
-
-
- {value()}
-
- >
- )}
-
-
-
- {childTitle()}
-
- }
- >
- {
- titleRef = el
+ >
+ }
+ actions={
+
+ {(_id) => (
+ <>
+ {props.search}
+ setTitle("draft", event.currentTarget.value)}
- onKeyDown={(event) => {
- event.stopPropagation()
-
- if (event.isComposing || event.keyCode === 229) return
-
- if (event.key === "Enter") {
- event.preventDefault()
- void saveTitleEditor()
-
- return
- }
-
- if (event.key === "Escape") {
- event.preventDefault()
- closeTitleEditor()
- }
- }}
- onBlur={() => void saveTitleEditor()}
/>
-
-
-
- {(id) => (
-
- )}
-
-
-
-
- {(_id) => (
-
- {props.search}
-
-
-
+
+ >
+ )}
+
+ }
+ >
+
+ {(value) => (
+ <>
+
+
+ {value()}
+
+ >
)}
-
+
+
+ {childTitle()}
+
+ }
+ >
+ {
+ titleRef = el
+ }}
+ data-slot="session-title-child"
+ dir="auto"
+ value={title.draft}
+ disabled={props.pending.rename()}
+ class="block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base field-sizing-content rounded-[6px] px-1 py-1"
+ classList={{ "max-w-[45%] shrink-0": !!parentID() }}
+ style={{
+ "--inline-input-shadow": "none",
+ "text-align": "start",
+ }}
+ onInput={(event) => setTitle("draft", event.currentTarget.value)}
+ onKeyDown={(event) => {
+ event.stopPropagation()
+
+ if (event.isComposing || event.keyCode === 229) return
+
+ if (event.key === "Enter") {
+ event.preventDefault()
+ void saveTitleEditor()
+
+ return
+ }
+
+ if (event.key === "Escape") {
+ event.preventDefault()
+ closeTitleEditor()
+ }
+ }}
+ onBlur={() => void saveTitleEditor()}
+ />
+
+
+
+ {(id) => (
+
+ )}
+
+
}
diff --git a/packages/app/src/session/timeline/virtualizer.tsx b/packages/app/src/session/timeline/virtualizer.tsx
index f672f61bf33..7f4e5d09825 100644
--- a/packages/app/src/session/timeline/virtualizer.tsx
+++ b/packages/app/src/session/timeline/virtualizer.tsx
@@ -924,7 +924,7 @@ export function createTimelineVirtualizer(input: Input) {
onScroll={handleListScroll}
onClick={input.onSelectionInteraction}
class="relative min-w-0 w-full h-full"
- style={{ "--sticky-accordion-top": input.showHeader() ? "48px" : "0px" }}
+ style={{ "--sticky-accordion-top": input.showHeader() ? "var(--session-header-height, 48px)" : "0px" }}
>
}>
{props.header}
From 24585a01f75adc98cb593389c4ccdb94d3a956c3 Mon Sep 17 00:00:00 2001
From: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Date: Tue, 20 Oct 2026 15:30:30 +0200
Subject: [PATCH 3/3] fix(session-ui): match review status labels to diff
counts
---
packages/session-ui/src/components/session-review.css | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/session-ui/src/components/session-review.css b/packages/session-ui/src/components/session-review.css
index 702edf27ab4..e5bc9ef3e08 100644
--- a/packages/session-ui/src/components/session-review.css
+++ b/packages/session-ui/src/components/session-review.css
@@ -198,7 +198,7 @@
[data-slot="session-review-change"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
- font-weight: var(--font-weight-medium);
+ font-weight: 440;
}
[data-slot="session-review-summary-open-file"] {
@@ -207,11 +207,11 @@
}
[data-slot="session-review-change"][data-type="added"] {
- color: var(--icon-diff-add-base);
+ color: var(--text-diff-add-base);
}
[data-slot="session-review-change"][data-type="removed"] {
- color: var(--icon-diff-delete-base);
+ color: var(--text-diff-delete-base);
}
[data-slot="session-review-change"][data-type="modified"] {