feat(ui): show the session title in the header bar (#340)

Fixes #299

## Summary
- show the active session title in the instance header, just after the
menu switch.
- keep the title visible whenever the left session drawer is not pinned,
using a quiet two-line header treatment without active-item
highlighting.
- when the unpinned drawer is open as a floating overlay, keep the title
in the same left header slot so the drawer covers it instead of pushing
toolbar controls around.
- disable the feature in mobile view

## Validation
- `git diff --check`
- `npm run typecheck --workspace @codenomad/ui`
- `npm run build --workspace @codenomad/ui`
- visually validated in the rebuilt desktop app raw executable
This commit is contained in:
Pascal André 2026-06-07 13:02:06 +02:00 committed by GitHub
parent 4ae7baa8b4
commit 97dcc0a692
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 146 additions and 100 deletions

View file

@ -103,7 +103,6 @@ const App: Component = () => {
binaryPath: string
instanceId: string
} | null>(null)
const phoneQuery = useMediaQuery("(max-width: 767px)")
const isPhoneLayout = createMemo(() => phoneQuery())

View file

@ -514,6 +514,23 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
</div>
)
const renderPreviewToggleButton = () => (
<Show when={!showingInfoView()}>
<IconButton
color="inherit"
onClick={handlePreviewButtonClick}
aria-label={previewToggleLabel()}
title={previewToggleLabel()}
size="small"
>
{(() => {
const Icon = PreviewToggleIcon()
return <Icon class="w-5 h-5" aria-hidden="true" />
})()}
</IconButton>
</Show>
)
const handleCommandPaletteClick = () => {
showCommandPalette(props.instance.id)
}
@ -834,6 +851,50 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
}
const showingInfoView = createMemo(() => activeSessionIdForInstance() === "info")
const activeSessionTitle = createMemo(() => {
if (showingInfoView()) return null
const title = activeSessionForInstance()?.title?.trim()
return title || t("sessionList.session.untitled")
})
const showHeaderLeftSlot = createMemo(() => !leftPinned())
const showHeaderSessionTitle = createMemo(() => !compactHeaderLayout() && showHeaderLeftSlot() && Boolean(activeSessionTitle()))
const headerToolbarHorizontalInset = createMemo(() => (isPhoneLayout() ? 16 : 24))
const headerLeftSlotWidth = createMemo(() => Math.max(0, sessionSidebarWidth() - headerToolbarHorizontalInset()))
const headerLeftSlotStyle = createMemo(() =>
leftDrawerState() === "floating-open" || showHeaderSessionTitle() ? { width: `${headerLeftSlotWidth()}px` } : undefined,
)
const renderActiveSessionHeaderTitle = () => (
<Show when={showHeaderSessionTitle()}>
<div
class="session-header-active-title"
dir="auto"
title={activeSessionTitle() ?? undefined}
>
<span class="session-header-active-title-text">{activeSessionTitle()}</span>
</div>
</Show>
)
const renderHeaderLeftSlot = () => (
<Show when={showHeaderLeftSlot()}>
<div class="session-header-left-slot" style={headerLeftSlotStyle()}>
<Show when={leftDrawerState() === "floating-closed"}>
<IconButton
ref={setLeftToggleButtonEl}
color="inherit"
onClick={handleLeftAppBarButtonClick}
aria-label={leftAppBarButtonLabel()}
size="small"
aria-expanded={leftDrawerState() !== "floating-closed"}
>
{leftAppBarButtonIcon()}
</IconButton>
</Show>
{renderActiveSessionHeaderTitle()}
</div>
</Show>
)
const isLaunching = createMemo(() => props.instance.status === "starting")
@ -933,94 +994,76 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
fallback={
<div class="flex flex-col w-full gap-1.5">
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
<Show when={leftDrawerState() === "floating-closed"}>
<IconButton
ref={setLeftToggleButtonEl}
color="inherit"
onClick={handleLeftAppBarButtonClick}
aria-label={leftAppBarButtonLabel()}
size="small"
aria-expanded={leftDrawerState() !== "floating-closed"}
>
{leftAppBarButtonIcon()}
</IconButton>
</Show>
{renderHeaderLeftSlot()}
<div class="flex-1 flex items-center justify-center min-w-0">
{renderSessionHeaderIndicators()}
</div>
<div class="flex-1 flex items-center justify-center min-w-0">
{renderSessionHeaderIndicators()}
</div>
<div class="flex flex-wrap items-center justify-center gap-1">
<Show when={!showingInfoView()}>
<div class="flex flex-wrap items-center justify-center gap-1">
<Show when={!showingInfoView()}>
<IconButton
color="inherit"
onClick={handleChatSearchClick}
aria-label={t("instanceShell.chatSearch.openAriaLabel")}
title={t("instanceShell.chatSearch.openAriaLabel")}
size="small"
>
<Search class="w-5 h-5" aria-hidden="true" />
</IconButton>
</Show>
<button
type="button"
class="connection-status-button command-palette-button"
onClick={handleCommandPaletteClick}
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
style={{ flex: "0 0 auto", width: "auto" }}
>
{t("instanceShell.commandPalette.button")}
</button>
<span class="connection-status-shortcut-hint kbd-hint">
<Kbd shortcut="cmd+shift+p" />
</span>
</div>
<div class="flex-1 flex items-center justify-center min-w-0">
<span
class={`status-indicator ${connectionStatusClass()}`}
aria-label={t("instanceShell.connection.ariaLabel", { status: connectionStatusLabel() })}
>
<span class="status-dot" />
</span>
</div>
<Show when={!isPhoneLayout()}>
{renderPreviewToggleButton()}
</Show>
<Show when={isPhoneLayout() && !props.mobileFullscreenMode}>
<IconButton
color="inherit"
onClick={handleChatSearchClick}
aria-label={t("instanceShell.chatSearch.openAriaLabel")}
title={t("instanceShell.chatSearch.openAriaLabel")}
onClick={props.onEnterMobileFullscreen}
aria-label={t("instanceShell.fullscreen.enter")}
title={t("instanceShell.fullscreen.enter")}
size="small"
>
<Search class="w-5 h-5" aria-hidden="true" />
<Maximize2 class="w-5 h-5" aria-hidden="true" />
</IconButton>
{renderPreviewToggleButton()}
</Show>
<Show when={rightDrawerState() === "floating-closed"}>
<IconButton
ref={setRightToggleButtonEl}
color="inherit"
onClick={handlePreviewButtonClick}
aria-label={previewToggleLabel()}
title={previewToggleLabel()}
onClick={handleRightAppBarButtonClick}
aria-label={rightAppBarButtonLabel()}
size="small"
aria-expanded={rightDrawerState() !== "floating-closed"}
>
{(() => {
const Icon = PreviewToggleIcon()
return <Icon class="w-5 h-5" aria-hidden="true" />
})()}
{rightAppBarButtonIcon()}
</IconButton>
</Show>
<button
type="button"
class="connection-status-button command-palette-button"
onClick={handleCommandPaletteClick}
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
style={{ flex: "0 0 auto", width: "auto" }}
>
{t("instanceShell.commandPalette.button")}
</button>
<span class="connection-status-shortcut-hint kbd-hint">
<Kbd shortcut="cmd+shift+p" />
</span>
</div>
<div class="flex-1 flex items-center justify-center min-w-0">
<span
class={`status-indicator ${connectionStatusClass()}`}
aria-label={t("instanceShell.connection.ariaLabel", { status: connectionStatusLabel() })}
>
<span class="status-dot" />
</span>
</div>
<Show when={isPhoneLayout() && !props.mobileFullscreenMode}>
<IconButton
color="inherit"
onClick={props.onEnterMobileFullscreen}
aria-label={t("instanceShell.fullscreen.enter")}
title={t("instanceShell.fullscreen.enter")}
size="small"
>
<Maximize2 class="w-5 h-5" aria-hidden="true" />
</IconButton>
</Show>
<Show when={rightDrawerState() === "floating-closed"}>
<IconButton
ref={setRightToggleButtonEl}
color="inherit"
onClick={handleRightAppBarButtonClick}
aria-label={rightAppBarButtonLabel()}
size="small"
aria-expanded={rightDrawerState() !== "floating-closed"}
>
{rightAppBarButtonIcon()}
</IconButton>
</Show>
</div>
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
@ -1038,18 +1081,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
}
>
<div class="session-toolbar-left flex-1 flex items-center gap-3 min-w-0">
<Show when={leftDrawerState() === "floating-closed"}>
<IconButton
ref={setLeftToggleButtonEl}
color="inherit"
onClick={handleLeftAppBarButtonClick}
aria-label={leftAppBarButtonLabel()}
size="small"
aria-expanded={leftDrawerState() !== "floating-closed"}
>
{leftAppBarButtonIcon()}
</IconButton>
</Show>
{renderHeaderLeftSlot()}
<Show when={!showingInfoView()}>
<ContextMeter
@ -1095,18 +1127,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
>
<Search class="w-5 h-5" aria-hidden="true" />
</IconButton>
<IconButton
color="inherit"
onClick={handlePreviewButtonClick}
aria-label={previewToggleLabel()}
title={previewToggleLabel()}
size="small"
>
{(() => {
const Icon = PreviewToggleIcon()
return <Icon class="w-5 h-5" aria-hidden="true" />
})()}
</IconButton>
{renderPreviewToggleButton()}
</Show>
<Show when={connectionStatus() === "connected"}>
<span class="status-indicator connected">

View file

@ -105,6 +105,32 @@
color: var(--text-primary);
}
.session-header-left-slot {
@apply flex items-center gap-2 min-w-0;
flex: 0 0 auto;
}
.session-header-active-title {
display: flex;
align-items: center;
flex: 1 1 auto;
min-width: 0;
align-self: stretch;
padding-inline: 0.75rem;
border-inline: 1px solid color-mix(in oklab, var(--border-base) 72%, transparent);
color: var(--text-secondary);
}
.session-header-active-title-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 0.8125rem;
font-weight: 500;
line-height: 1.15;
}
.session-sidebar-shortcuts {
@apply flex flex-col gap-1;
}