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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5AYXP2WM5WYKYA2RC58AAYC
anchor: function row
created: 2026-10-19T20:51:09Z
norm: '1'
sig: 3ee93dbf7c44f79a
body_hash: 14ce3b1013f26d6a
raw_hash: 628a958fefb02100
lines: 120-170
---

each capability mirrors a host-side refusal (gateway owned ops, catalog.owns, compute fork); keep them in step when the host's rules change
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5AYXPASFB8MZPK7Y2G4B4B1
anchor: function RowMenu
created: 2026-10-19T20:51:18Z
norm: '1'
sig: cc66f447e5746620
body_hash: 9149eb5ee3bff411
raw_hash: eab0909ef4f29ba0
lines: 215-453
---

an action this row can't perform is hidden, never shown disabled
13 changes: 8 additions & 5 deletions apps/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ function row(
outcome?: Outcome,
): SidebarRow {
const place = placement(channel);
// A host's channels are all its owner's, so this is also the host's owner-only check.
const own = channel.owner === user && channel.host === host;
return {
uid: channel.id,
kind: "session",
Expand All @@ -138,12 +140,13 @@ function row(
member: true,
capabilities: {
rename: channel.owner === user && channel.state !== "offline",
archive: channel.host === host,
delete: channel.host === host,
move: channel.owner === user && channel.host === host && channel.state !== "archived"
&& !channel.compute,
archive: own,
delete: own,
// A cloud compute channel's lanes are in its container, out of the host's reach.
fork: own && !channel.compute,
move: own && channel.state !== "archived" && !channel.compute,
// The applicable action follows the transition's end, so none is offered during it.
compute: !!channel.compute && channel.host === host && !channel.computing,
compute: own && !!channel.compute && !channel.computing,
},
connection: channel.computing
? "warming"
Expand Down
88 changes: 47 additions & 41 deletions packages/ui/src/components/session-item/session-item-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ export function RowMenu(

let leave = data.member ? onLeave : undefined;
let canPin = canTogglePin({ pinned, onPin, onUnpin });
// Actions that can't succeed on this row are hidden, so a row may be left with none to offer.
let offered = lobby
? Boolean(onRebuild)
: archived
? Boolean(onArchive || onDelete)
: Boolean(canPin || onRename || onFork || onInfo || leave || movable || onCompute || onArchive);

function pin(event: MouseEvent<HTMLElement>) {
stop(event);
Expand Down Expand Up @@ -219,7 +225,7 @@ export function RowMenu(
}

return (
<ContextMenu.Root onOpenChange={change}>
<ContextMenu.Root disabled={!offered} onOpenChange={change}>
<ContextMenu.Trigger
render={
<div
Expand Down Expand Up @@ -253,20 +259,26 @@ export function RowMenu(
{pinned ? "Unpin channel" : "Pin channel"}
</ContextMenu.Item>
)}
<ContextMenu.Item className={MENU_ITEM} disabled={!onRename} onClick={rename}>
<Pencil className="size-3.5" />
Rename channel
</ContextMenu.Item>
<ContextMenu.Item className={MENU_ITEM} disabled={!onFork} onClick={fork}>
<Fork className="size-3.5" />
Fork channel
</ContextMenu.Item>
<ContextMenu.Item className={MENU_ITEM} disabled={!onInfo} onClick={info}>
<Info className="size-3.5" />
Channel info
</ContextMenu.Item>
{data.member && (
<ContextMenu.Item className={MENU_ITEM} disabled={!leave} onClick={exit}>
{onRename && (
<ContextMenu.Item className={MENU_ITEM} onClick={rename}>
<Pencil className="size-3.5" />
Rename channel
</ContextMenu.Item>
)}
{onFork && (
<ContextMenu.Item className={MENU_ITEM} onClick={fork}>
<Fork className="size-3.5" />
Fork channel
</ContextMenu.Item>
)}
{onInfo && (
<ContextMenu.Item className={MENU_ITEM} onClick={info}>
<Info className="size-3.5" />
Channel info
</ContextMenu.Item>
)}
{leave && (
<ContextMenu.Item className={MENU_ITEM} onClick={exit}>
<Leave className="size-3.5" />
Leave channel
</ContextMenu.Item>
Expand Down Expand Up @@ -302,14 +314,12 @@ export function RowMenu(
<ContextMenu.Separator className={MENU_SEPARATOR} />
</>
)}
<ContextMenu.Item
className={MENU_ITEM_DESTRUCTIVE_FULL}
disabled={!onArchive}
onClick={archive}
>
<ArchiveContent2 className="size-3.5" />
Archive channel
</ContextMenu.Item>
{onArchive && (
<ContextMenu.Item className={MENU_ITEM_DESTRUCTIVE_FULL} onClick={archive}>
<ArchiveContent2 className="size-3.5" />
Archive channel
</ContextMenu.Item>
)}
{onDelete && (
<ContextMenu.Item className={MENU_ITEM_DESTRUCTIVE_FULL} disabled>
<Trash className="size-3.5" />
Expand All @@ -321,26 +331,22 @@ export function RowMenu(
)}
{!lobby && archived && (
<>
<ContextMenu.Item className={MENU_ITEM} disabled={!onArchive} onClick={archive}>
<Unarchive className="size-3.5" />
Unarchive channel
</ContextMenu.Item>
<ContextMenu.Item
className={MENU_ITEM_DESTRUCTIVE_FULL}
disabled={!onDelete}
onClick={del}
>
<Trash className="size-3.5" />
Delete channel
</ContextMenu.Item>
{onArchive && (
<ContextMenu.Item className={MENU_ITEM} onClick={archive}>
<Unarchive className="size-3.5" />
Unarchive channel
</ContextMenu.Item>
)}
{onDelete && (
<ContextMenu.Item className={MENU_ITEM_DESTRUCTIVE_FULL} onClick={del}>
<Trash className="size-3.5" />
Delete channel
</ContextMenu.Item>
)}
</>
)}
{lobby && (
<ContextMenu.Item
className={MENU_ITEM_DESTRUCTIVE_FULL}
disabled={!onRebuild}
onClick={rebuild}
>
{lobby && onRebuild && (
<ContextMenu.Item className={MENU_ITEM_DESTRUCTIVE_FULL} onClick={rebuild}>
<Rebuild className="size-3.5" />
Rebuild Lobby
</ContextMenu.Item>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/session-item/session-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ function actions(data: SidebarRow, props: RowActions): RowActions {
}
return {
...pins,
onFork: props.onFork,
onFork: data.capabilities?.fork === false ? undefined : props.onFork,
onInfo: props.onInfo,
onLeave: props.onLeave,
onArchive: data.capabilities?.archive === false ? undefined : props.onArchive,
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type Capabilities = {
rename: boolean;
archive: boolean;
delete?: boolean;
fork?: boolean;
move?: boolean;
/** The channel's workspace is a cloud container its host can stop and start. */
compute?: boolean;
Expand Down