Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,6 @@ import {
planPluginManagerChange,
} from "./plugins/lifecycle";
import { usePluginSnapshot } from "./plugins/usePluginSnapshot";
import { ProjectIcon } from "./projects/ProjectIcon";
import { loadProviderRegistry } from "./providers/registry";
import { RemoteModal } from "./remote/Remote";
import { Composer } from "./session/Composer";
Expand Down Expand Up @@ -8420,16 +8419,6 @@ export default function App() {
>
{railExpandAction}
{/* Breadcrumb, reference-style: project / thread. */}
<span className="session-header-project-icon flex shrink-0 items-center">
{activeProjectRecord ? (
<ProjectIcon
project={activeProjectRecord}
size={18}
/>
) : (
<Folder className="text-muted-foreground size-3.5" />
)}
</span>
{activeProjectName != null &&
activeProjectName !== "" && (
<>
Expand Down Expand Up @@ -8475,7 +8464,7 @@ export default function App() {

<div className="electrobun-webkit-app-region-drag flex-1" />

<div className="session-header-toolbar [&_svg]:text-muted-foreground flex min-w-0 shrink-0 items-center gap-4">
<div className="session-header-toolbar [&_svg]:text-muted-foreground gap-inline flex min-w-0 shrink-0 items-center">
{/* Full-page mode hides the transcript, so the header carries the only sign that a turn
is in flight — and the way back to the answer without leaving the mode for good. */}
{docMode && hasConversationContent && (
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/projects/ProjectIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function ProjectIcon({
data-project-icon
aria-hidden="true"
className={cn(
"rounded-control bg-foreground/[0.055] text-muted-foreground ring-foreground/10 flex shrink-0 items-center justify-center overflow-hidden ring-1",
"rounded-control text-muted-foreground flex shrink-0 items-center justify-center overflow-hidden",
className
)}
style={{ width: size, height: size }}
Expand Down
33 changes: 18 additions & 15 deletions apps/desktop/src/session/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export function CheckoutBar({
aria-expanded={open}
>
<Folder className="size-3.5 shrink-0" aria-hidden="true" />
<span className="text-body text-foreground/85 min-w-0 truncate">
<span className="text-body text-muted-foreground min-w-0 truncate">
{modeLabel}
</span>
<ChevronDown
Expand Down Expand Up @@ -366,7 +366,7 @@ export function CheckoutBar({
size="compact"
focusStyle="inset"
onClick={checkout.onOpen}
className="bg-foreground/[0.04] px-module-inset text-callout text-foreground/80 ml-auto shrink-0 gap-1.5 font-mono"
className="bg-foreground/[0.04] px-module-inset text-callout text-muted-foreground ml-auto shrink-0 gap-1.5 font-mono"
aria-label={t("checkout.openSourceControl", {
branch: checkout.branch,
})}
Expand Down Expand Up @@ -1695,6 +1695,20 @@ export function Composer({

{pluginActions}

{/* Session configuration shares the control row with attach, voice and run: one strip, one
baseline. The chips row wraps inside its own measure when the composer is narrow. */}
<SessionControls
config={config}
models={models}
currentModel={currentModel}
defaultModel={defaultModel}
onModel={onModel}
configOptions={configOptions}
onConfigOption={onConfigOption}
modelChangeDisabled={running || loading}
showWorktreePicker={!checkout}
/>

<div className="flex-1" />

{/* Document mode is the app's own feature — it deserves a control you can see, not just a
Expand Down Expand Up @@ -2022,26 +2036,15 @@ export function Composer({
>
<div
className={cn(
"flex flex-col gap-1",
"flex min-w-0 items-center gap-0.5",
docMode
? "rounded-composer bg-surface shadow-surface pointer-events-auto mx-auto w-full max-w-3xl p-2"
: // Keep every outer edge 8px from the controls. The 24px surface radius then
// shares its bottom-right centre with the circular send/stop control.
"p-2"
)}
>
<SessionControls
config={config}
models={models}
currentModel={currentModel}
defaultModel={defaultModel}
onModel={onModel}
configOptions={configOptions}
onConfigOption={onConfigOption}
modelChangeDisabled={running || loading}
showWorktreePicker={!checkout}
/>
<div className="flex items-center gap-0.5">{controls}</div>
{controls}
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/session/SessionHeaderActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function SessionHeaderActions({
);
return (
<div
className="session-header-actions flex shrink-0 items-center gap-2"
className="session-header-actions gap-inline flex shrink-0 items-center"
role="group"
aria-label={t("header.actions")}
>
Expand Down
13 changes: 7 additions & 6 deletions apps/desktop/src/sidebar/SessionRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,7 @@ export function SessionRail({
aria-label={t(
isWorktree ? "rail.gitWorktreeHint" : "rail.gitCheckoutHint"
)}
className="rounded-micro bg-fill-quiet text-fine text-foreground/55 flex shrink-0 items-center gap-0.5 px-1 leading-4"
className="text-fine text-foreground/55 flex shrink-0 items-center gap-0.5 leading-4"
>
<GitBranch className="size-2.5" aria-hidden="true" />
{t(isWorktree ? "rail.gitWorktree" : "rail.gitCheckout")}
Expand All @@ -1032,7 +1032,7 @@ export function SessionRail({
title={`#${pullRequest.number} · ${pullRequestLabel}`}
aria-label={`#${pullRequest.number} · ${pullRequestLabel}`}
className={cn(
"rounded-micro bg-fill-quiet text-fine flex shrink-0 items-center gap-0.5 px-1 leading-4",
"text-fine flex shrink-0 items-center gap-0.5 leading-4",
pullRequestTone
)}
>
Expand All @@ -1043,7 +1043,7 @@ export function SessionRail({
) : (
<GitPullRequest className="size-2.5" aria-hidden="true" />
)}
#{pullRequest.number} {pullRequestLabel}
#{pullRequest.number}
</span>
) : null;
const provenanceInSummary = pullRequest === null;
Expand Down Expand Up @@ -1328,7 +1328,7 @@ export function SessionRail({
className={cn(
"session-rail-row group rounded-control hover:bg-fill-hover focus-within:bg-fill-hover active:bg-fill-pressed data-[popup-open]:bg-fill-hover relative cursor-default px-2 py-1.5 transition-[box-shadow,opacity] outline-none data-[sidebar-dragging=true]:opacity-45",
s.id === activeSession &&
"bg-fill-selected hover:bg-fill-selected-hover focus-within:bg-fill-selected active:bg-fill-pressed before:absolute before:inset-y-3 before:left-0.5 before:w-0.5 before:rounded-full before:bg-current"
"bg-fill-selected hover:bg-fill-selected-hover focus-within:bg-fill-selected active:bg-fill-pressed"
)}
>
<Button
Expand All @@ -1351,7 +1351,7 @@ export function SessionRail({
/>
<div
data-session-content
className="pointer-events-none relative z-10 pl-1.5"
className="pointer-events-none relative z-10"
>
{/* Title owns the row. Routine controls appear on demand. */}
<div
Expand Down Expand Up @@ -1756,14 +1756,15 @@ export function SessionRail({
ref={sourceRef}
type="button"
variant="ghost"
size="row"
focusStyle="inset"
/>
}
data-project-toggle={project.path}
title={t(open ? "rail.hideProject" : "rail.showProject", {
name: project.name,
})}
className="rounded-control text-ui focus-visible:focus-ring-inset flex min-w-0 flex-1 items-center gap-2 px-2 leading-4 outline-none hover:bg-transparent dark:hover:bg-transparent"
className="rounded-control text-body focus-visible:focus-ring-inset flex min-w-0 flex-1 items-center gap-2 leading-4 outline-none hover:bg-transparent dark:hover:bg-transparent"
>
<Folder className="size-4 shrink-0" aria-hidden="true" />
<span className="min-w-0 flex-1 truncate text-left">
Expand Down
9 changes: 0 additions & 9 deletions apps/desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1793,10 +1793,6 @@ pre {
}

@container session-header (max-width: 36rem) {
.session-header-actions {
gap: var(--ds-space-surface-inset);
}

.session-header-action-label {
display: none;
}
Expand All @@ -1814,7 +1810,6 @@ pre {
}

@container session-header (max-width: 28rem) {
.session-header-project-icon,
.session-header-project-context,
.session-header [data-plugin-ui-slot="session.header"] {
display: none;
Expand All @@ -1823,10 +1818,6 @@ pre {
.session-header-title {
min-width: 3rem;
}

.session-header-actions {
gap: 0.25rem;
}
}

@container dock (max-width: 640px) {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/tests/sessionHeaderActionsRendered.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ describe("SessionHeaderActions", () => {
activateDom();
const { view } = renderActions();
const group = view.container.querySelector(".session-header-actions");
expect(group?.classList.contains("gap-2")).toBe(true);
expect(group?.classList.contains("gap-inline")).toBe(true);
expect(group?.classList.contains("gap-2")).toBe(false);
expect(group?.classList.contains("rounded-control")).toBe(false);
expect(group?.classList.contains("p-0.5")).toBe(false);

Expand Down
68 changes: 43 additions & 25 deletions apps/desktop/tests/sessionRailRendered.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,17 +374,15 @@ describe("SessionRail row layout", () => {
'[data-session-id="meaningful"]'
);
expect(groupedRow?.className).not.toContain("ml-6");
expect(toggle?.className).toContain("px-2");
expect(toggle?.getAttribute("data-size")).toBe("row");
expect(toggle?.className).toContain("min-h-navigation-row");
expect(toggle?.className).toContain("px-module-inset");
expect(groupedRow?.className).toContain("px-2");
// One content column: the project mark, the nav marks and the session text all sit on the
// rail's single 16px edge, so the session content adds no padding of its own.
expect(
groupedRow?.querySelector("[data-session-content]")?.className
).toContain("pl-1.5");
expect(
groupedRow?.querySelector("[data-session-content]")?.className
).not.toContain("pl-2");
expect(
groupedRow?.querySelector("[data-session-content]")?.className
).not.toContain("pl-6");
).not.toContain("pl-");

click(toggle);
await waitFor(() => {
Expand Down Expand Up @@ -1085,15 +1083,12 @@ describe("SessionRail row layout", () => {
).toBeTruthy();
expect(row?.querySelectorAll("[data-session-line]")).toHaveLength(2);
expect(row?.textContent).not.toContain("repo");
expect(row?.querySelector("[data-session-content]")?.className).toContain(
"pl-1.5"
);
expect(
row?.querySelector("[data-session-content]")?.className
).not.toContain("pl-2");
expect(
row?.querySelector("[data-session-content]")?.className
).not.toContain("pl-6");
).not.toContain("pl-");
expect(row?.querySelector("[data-session-content]")?.className).toContain(
"z-10"
);

view.unmount();
});
Expand Down Expand Up @@ -1158,18 +1153,41 @@ describe("SessionRail row layout", () => {
).toContain("Checkout");

await waitFor(() => {
expect(
view.container.querySelector(
'[data-session-id="isolated"] [data-session-pull-request="merged"]'
)?.textContent
).toContain("#84 Merged");
expect(
view.container.querySelector(
'[data-session-id="regular"] [data-session-pull-request="ci_failed"]'
)?.textContent
).toContain("#83 CI failed");
const merged = view.container.querySelector(
'[data-session-id="isolated"] [data-session-pull-request="merged"]'
);
const failed = view.container.querySelector(
'[data-session-id="regular"] [data-session-pull-request="ci_failed"]'
);
// The state travels by colour and the shared tooltip/accessible name, not by visible words.
expect(merged?.textContent).toContain("#84");
expect(merged?.textContent).not.toContain("Merged");
expect(merged?.getAttribute("title")).toContain("Merged");
expect(merged?.getAttribute("aria-label")).toContain("Merged");
expect(failed?.textContent).toContain("#83");
expect(failed?.textContent).not.toContain("CI failed");
expect(failed?.getAttribute("title")).toContain("CI failed");
});

// The provenance marks are inline meta, not pills: no fill and no pill padding, so their text
// edge matches the row title's edge on the shared content container.
for (const selector of [
'[data-session-id="isolated"] [data-session-checkout-kind="worktree"]',
'[data-session-id="regular"] [data-session-pull-request="ci_failed"]',
]) {
const badge = view.container.querySelector(selector);
expect(badge?.classList.contains("bg-fill-quiet")).toBe(false);
expect(badge?.classList.contains("rounded-micro")).toBe(false);
expect(badge?.classList.contains("px-1")).toBe(false);
}
const row = view.container.querySelector('[data-session-id="regular"]');
const titleLine = row?.querySelector('[data-session-line="title"]');
const workspaceLine = row?.querySelector('[data-session-line="workspace"]');
expect(titleLine?.parentElement).toBe(workspaceLine?.parentElement);
expect(titleLine?.parentElement?.hasAttribute("data-session-content")).toBe(
true
);

view.unmount();
});

Expand Down
Loading
Loading