Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ae6d120
fix(next): pin project actions and contain menu scrollbars
teamleaderleo Oct 28, 2026
2cc4bf1
refine(next): keep project picker patch scoped
teamleaderleo Oct 28, 2026
5dc5ea3
fix(next): retain search menus through scrollbar gestures
teamleaderleo Oct 28, 2026
5686598
fix(next): keep search highlight on projects with pinned action buttons
teamleaderleo Oct 28, 2026
22e0dc8
test(next): preserve project action selection semantics
teamleaderleo Oct 28, 2026
c32a5ee
feat(next): unify composer project and environment menus
teamleaderleo Oct 28, 2026
913d8ef
fix(next): preserve machine row section semantics
teamleaderleo Oct 28, 2026
7c03b8f
fix(next): preserve environment ordering contract
teamleaderleo Oct 28, 2026
e749db1
feat(next): keep current environment first
teamleaderleo Oct 28, 2026
e5aaa27
docs(next): clarify composer menu icon and placement contract
teamleaderleo Oct 28, 2026
bf5bf90
fix(next): keep environment scrolling and restore picker focus
teamleaderleo Oct 28, 2026
ed0b0ff
fix(next): use one project menu scrollbar and style pinned actions
teamleaderleo Oct 28, 2026
529413a
test(next): add composer menu design variants
teamleaderleo Oct 28, 2026
0635030
fix(next): keep pinned project actions in keyboard collection
teamleaderleo Oct 28, 2026
bb8dc7b
test(next): query proposed project among pinned actions
teamleaderleo Oct 28, 2026
e0eea0a
fix(next): keep scratch projects searchable but out of main list
teamleaderleo Oct 28, 2026
a077aa8
test(next): cover pinned arrow navigation and filtered projects
teamleaderleo Oct 28, 2026
065365e
fix(next): navigate pinned actions with arrow keys
teamleaderleo Oct 28, 2026
6fda486
fix(next): traverse pinned actions and project results
teamleaderleo Oct 28, 2026
aff47a8
fix(next): keep pinned actions in Base UI arrow order
teamleaderleo Oct 28, 2026
75aac51
test(next): narrow proposed project query
teamleaderleo Oct 28, 2026
f2152b2
fix(next): type composer menu gallery and scratch rows
teamleaderleo Oct 28, 2026
914eb52
test(next): give composer menu galleries room to render
teamleaderleo Oct 28, 2026
0e5e408
fix(next): contain composer dropdown results
teamleaderleo Oct 28, 2026
0af9359
test(next): cover composer dropdown containment
teamleaderleo Oct 28, 2026
a289b47
fix(next): bound composer dropdown surfaces
teamleaderleo Oct 28, 2026
1816db1
test(next): preserve scratch project names
teamleaderleo Oct 28, 2026
e679d7b
test(next): keep all unclassified project rows
teamleaderleo Oct 28, 2026
d310cc4
test(next): verify dropdown placement direction
teamleaderleo Oct 28, 2026
10a0bf4
test(next): cover bottom-edge dropdown fallback
teamleaderleo Oct 28, 2026
91ac1a7
fix(next): constrain project menu results
teamleaderleo Oct 28, 2026
d43caf0
test(next): assert nested result viewport bounds
teamleaderleo Oct 28, 2026
eaa5b56
fix(next): scale project menu text with controls
teamleaderleo Oct 28, 2026
a3830a7
test(next): place fallback trigger at viewport bottom
teamleaderleo Oct 28, 2026
ffafafa
fix(next): let project results shrink within popup
teamleaderleo Oct 28, 2026
1174005
test(next): preserve bottom-edge trigger placement
teamleaderleo Oct 28, 2026
2f13084
fix(next): preserve project menu intrinsic rows
teamleaderleo Oct 28, 2026
04a84b4
fix(next): cap composer menus to viewport height
teamleaderleo Oct 28, 2026
01ea745
test(next): open fallback menus through pointer path
teamleaderleo Oct 28, 2026
a2574db
fix(next): bound project result viewport and fallback gallery
teamleaderleo Oct 28, 2026
776532b
fix(next): constrain project scroll viewport
teamleaderleo Oct 28, 2026
33875e6
test(next): tolerate subpixel popup bounds
teamleaderleo Oct 28, 2026
0cf36c3
fix(next): bound location result viewport and fallback anchor
teamleaderleo Oct 28, 2026
3a6cfa8
fix(next): size location scroller and anchor fallback
teamleaderleo Oct 28, 2026
c8486b3
fix(next): keep folder scroller inside short popup
teamleaderleo Oct 28, 2026
c8fdae1
test(next): allow short folder popup edge clipping
teamleaderleo Oct 28, 2026
b3ecdbd
test(next): keep short popup containment assertion focused
teamleaderleo Oct 28, 2026
391e408
fix(next): flex folder results into short popup
teamleaderleo Oct 28, 2026
d6fb89e
test(next): require short folder results containment
teamleaderleo Oct 28, 2026
eb5d51d
test(next): avoid unstable location viewport geometry
teamleaderleo Oct 28, 2026
790004b
test(next): keep location popup bounds deterministic
teamleaderleo Oct 28, 2026
f4d4a8e
test(next): assert location last rows stay contained
teamleaderleo Oct 28, 2026
c9e8a44
test(next): await environment scroll condition
teamleaderleo Oct 28, 2026
49a4c06
test(next): assert location popup remains open after scrolling
teamleaderleo Oct 28, 2026
9c67747
fix(next): use tokenized location viewport sizing
teamleaderleo Oct 28, 2026
75b7166
test(next): verify location scrollers expose overflow
teamleaderleo Oct 28, 2026
eeeea35
chore(next): ratchet type scale baseline
teamleaderleo Oct 28, 2026
6cffd06
style(next): format composer menu changes
teamleaderleo Oct 28, 2026
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
24 changes: 24 additions & 0 deletions design/composer-menus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Composer project and environment menus

The project and environment controls in the composer share one menu contract. Their glass and
colour tokens remain those of the composer; rows use one 32 px rhythm, one search inset, and the
same columns: icon or kind glyph, primary and secondary text, trailing status or branch affordance,
then selection check.

Menus open below their trigger so they stay away from the transcript. The shared placement rule
chooses below when it fits, and flips above only when the available space below is smaller than the
menu and the space above is greater. The chosen side is stable while filtering so rows never jump.

The project list leads with the current or most recently supplied project. Repository worktrees are
grouped under their repository; scratch and temporary folders stay out of the initial list unless
selected, and a search for their path or name surfaces them. This uses structural paths and never
arbitrary-name filtering. A project avatar or repository mark is shown when available. Otherwise a
quiet monogram has no tile and does not compete with the name.

The environment list leads with the current machine. Local machines use a laptop glyph, SSH hosts a
terminal/server glyph, and cloud machines a cloud glyph. Section headers, search, secondary state,
keyboard selection, and the scrollbar use the same columns and row height as projects.

Gallery states cover three, eight, and fifteen project rows plus local, SSH, and cloud environments.
The selected layout is this shared-column version because it keeps the name column stable while
preserving readable secondary details and the current check in both light and dark themes.
2 changes: 1 addition & 1 deletion scripts/cmux-next/type-scale-baseline.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ webviews/src/agent-session/acpmux/markdownField.css size 4
webviews/src/agent-session/acpmux/mockFixture.ts size 2
webviews/src/agent-session/acpmux/newtab/screen.css size 15
webviews/src/agent-session/acpmux/prototype/prototype.css size 17
webviews/src/agent-session/acpmux/styles.css size 48
webviews/src/agent-session/acpmux/styles.css size 47
webviews/src/agent-session/acpmux/subagents/subagents.css size 2
webviews/src/agent-session/acpmux/threadMinimap/threadMinimap.css size 1
webviews/src/gallery/env.ts size 1
Expand Down
16 changes: 15 additions & 1 deletion webviews/src/agent-session/acpmux/ComposerContext.gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,23 @@ export default componentEntry<ComponentProps<typeof ComposerContext>>({
props: baseProps,
},
"folder-menu": {
note: "The folder popover: a search field over the recent projects (paths on hover), then Add project and Choose folder….",
note: "Project actions stay directly below search, above the recent projects (paths on hover).",
height: 560,
props: baseProps,
play: openFolder,
},
"folder-long-list": {
note: "Project actions stay below search while a long folder list scrolls; the thumb stays inside the right edge.",
props: {
...baseProps,
projectChoices: Array.from({ length: 40 }, (_, index) => ({
cwd: `/demo/review-${index + 1}`,
label: `review-${index + 1}`,
})),
onNoProject: () => undefined,
},
play: openFolder,
},
"folder-favicons": {
note: "Projects with a favicon (public/, app/, the folder itself) show it in their tile; cmux has none and draws a plain folder.",
props: baseProps,
Expand Down Expand Up @@ -411,6 +424,7 @@ export default componentEntry<ComponentProps<typeof ComposerContext>>({
},
"environment-picker": {
note: "Preview: the computer is the environment picker, one searchable list in Local, SSH and Cloud sections with ⌥⌘1–9; New Cloud Machine ends the Cloud section and New environment (SSH…, Connect Phone…) comes last. gpu-box is connected (a filled dot), Cloud machine is not (a ring); this Mac has no status.",
height: 560,
props: {
...baseProps,
environments: true,
Expand Down
14 changes: 11 additions & 3 deletions webviews/src/agent-session/acpmux/ComposerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type Location = {
repo?: ProjectRepo;
section?: string;
disabled?: boolean;
kind?: EnvironmentKind;
};
type Computer = Location & { kind: EnvironmentKind; connected?: boolean; state?: string; cloudMachine?: CloudMachine };
const NO_PEER_KINDS: Record<string, "ssh" | "cloud"> = {};
Expand Down Expand Up @@ -784,15 +785,22 @@ function LocationPicker({
return (
// tooltip-allow: the row shows the folder's name; the tooltip is its full path
<span
className="acpmux-menu-text acpmux-location-name"
className={
kind === "computer"
? "acpmux-location-row acpmux-location-name"
: "acpmux-menu-text acpmux-location-name"
}
title={path}
data-section={section}
data-first-section={section && index === 0 ? "" : undefined}
data-ready={folder?.disabled ? "false" : undefined}
aria-disabled={folder?.disabled ? true : undefined}
>
<span className="acpmux-menu-label">{folder?.label ?? path}</span>
{!allowPath && folder?.detail && <span className="acpmux-menu-description">{folder.detail}</span>}
{kind === "computer" && <EnvironmentIcon kind={folder?.kind ?? "local"} name={folder?.label} />}
<span className={kind === "computer" ? "acpmux-menu-text acpmux-location-name" : "acpmux-menu-label"}>
<span className="acpmux-menu-label">{folder?.label ?? path}</span>
{!allowPath && folder?.detail && <span className="acpmux-menu-description">{folder.detail}</span>}
</span>
{suffix && (
<>
{" "}
Expand Down
8 changes: 7 additions & 1 deletion webviews/src/agent-session/acpmux/EnvironmentPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,13 @@ export function EnvironmentPicker({
setAlternate(false);
};
}, [open, offersSandbox]);
const ordered = useMemo(() => orderEnvironments(environments), [environments]);
const ordered = useMemo(() => {
const rows = orderEnvironments(environments);
const currentIndex = rows.findIndex((environment) => environment.id === currentRow);
return currentIndex > 0
? [rows[currentIndex]!, ...rows.slice(0, currentIndex), ...rows.slice(currentIndex + 1)]
: rows;
}, [environments, currentRow]);
const sections = useMemo(() => environmentSections(environments), [environments]);
const sectionOf = (environment: Environment) => sections.get(environment.id) ?? "fleet";
const numbers = useMemo(
Expand Down
24 changes: 16 additions & 8 deletions webviews/src/agent-session/acpmux/FolderPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export type FolderChoice = { id: string; label: string; repo?: ProjectRepo };
/// another shares that name, a short suffix that tells it apart (projectSuffix.ts); the full path is
/// the row's tooltip. A repository's row picks the current folder, else its main checkout, else its
/// newest worktree; its other worktrees, newest first, open under it with → (← or the count closes
/// them). The current folder is checked. After the folders: New Worktree, Add project and Choose
/// folder…, each only where the host offers it. The shared Combobox keeps the keyboard in the field:
/// them). The current folder is checked. Actions stay directly under search, before the scrolling
/// folders: No project, New Worktree, Add project and Choose folder, where the host offers them. The shared Combobox keeps the keyboard in the field:
/// typing filters by repository, branch and path, and lists a worktree on its own when the words
/// name it; ↑/↓ move through the rows and the actions; Return picks; Escape closes back to the
/// strip. A typed absolute or `~/` path is offered as typed. `note` is a quiet line under the list:
Expand Down Expand Up @@ -83,7 +83,7 @@ export function FolderPopover({
() => projectSuffixes(groups.map((group) => ({ cwd: group.lead.id, label: group.name }))),
[groups],
);
const rows = useMemo(() => shownRepoRows(groups, query, expanded), [groups, query, expanded]);
const rows = useMemo(() => shownRepoRows(groups, query, expanded, current), [groups, query, expanded, current]);
// Each shown folder's row: a repository's (`group` only) or a worktree's under it.
const shown = useMemo(() => {
const byValue = new Map<string, { group: (typeof groups)[number]; worktree?: FolderChoice }>();
Expand All @@ -103,7 +103,7 @@ export function FolderPopover({
});
const typed = query.trim();
const typedPath = /^(?:\/|~\/)/.test(typed) && !shown.has(typed) ? typed : undefined;
// The actions follow the folders in the same list, so the arrows reach them from the field.
// Actions stay directly under search; Tab reaches them and arrows navigate the matching folders.
const actions: { value: string; text: string; run(): void }[] = [
...(onNoProject ? [{ value: ACTION + "none", text: t("project.noProject"), run: onNoProject }] : []),
...(onNewWorktree ? [{ value: ACTION + "worktree", text: t("worktree.new"), run: onNewWorktree }] : []),
Expand All @@ -114,7 +114,8 @@ export function FolderPopover({
]
: []),
];
const suggestions = [...shown.keys(), ...(typedPath ? [typedPath] : []), ...actions.map((action) => action.value)];
const pinned = actions.map((action) => action.value);
const suggestions = [...pinned, ...shown.keys(), ...(typedPath ? [typedPath] : [])];
const close = () => {
setOpen(false);
setQuery("");
Expand Down Expand Up @@ -260,9 +261,14 @@ export function FolderPopover({
</button>
<Popover
open={open}
onOpenChange={(next) => {
setOpen(next);
if (!next) setQuery("");
onOpenChange={(next, details) => {
if (!next && details?.escape) {
escaped.current = true;
close();
} else {
setOpen(next);
if (!next) setQuery("");
}
}}
anchor={open ? anchor : null}
label={label}
Expand All @@ -273,6 +279,8 @@ export function FolderPopover({
>
<Combobox
suggestions={suggestions}
pinnedSuggestions={pinned}
cancelOnBlur={false}
onQuery={setQuery}
onSubmit={submit}
onCancel={close}
Expand Down
26 changes: 8 additions & 18 deletions webviews/src/agent-session/acpmux/ProjectBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ export function projectKind(project: Pick<Project, "cwd" | "peer" | "host">): Pr

/// The project's own icon when the host finds one on this Mac (projectIcon.ts: its GitHub owner's
/// avatar, else an icon file in the folder or its repository), else its framework's or language's
/// mark, else a plain folder; an icon WebKit cannot draw falls back to the mark or the folder too.
/// Never letters and never a corner dot (Leo 2026-10-21: "do the icons/favicons/heuristics for icons
/// apply here yet?"; no dots on the rows).
/// mark; an icon WebKit cannot draw falls back to the mark or a quiet monogram.
/// If there is no icon, use a quiet monogram with no tile; never repeat a generic folder tile.
export function ProjectBadge({ project }: { project: Project }) {
const kind = projectKind(project);
const icon = useProjectIcon(kind === "local" ? project.cwd : undefined);
Expand All @@ -41,27 +40,18 @@ export function ProjectBadge({ project }: { project: Project }) {
<path fill="currentColor" d={glyph.path} />
</svg>
) : (
<FolderMark />
<MonogramMark value={project.label || project.cwd} />
)}
</span>
);
}

/// The folder a project with no icon and no mark draws, in the muted text color (the trigger's own).
function FolderMark() {
function MonogramMark({ value }: { value: string }) {
const letter = Array.from(value.trim())[0]?.toUpperCase() ?? "•";
return (
<svg
width={14}
height={14}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.25}
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M1.9 4.6c0-.8.6-1.4 1.4-1.4h2.6l1.5 1.6h5.3c.8 0 1.4.6 1.4 1.4v5.6c0 .8-.6 1.4-1.4 1.4H3.3c-.8 0-1.4-.6-1.4-1.4Z" />
</svg>
<span className="acpmux-project-monogram" aria-hidden="true">
{letter}
</span>
);
}
Loading
Loading