diff --git a/.lane/memory/apps/app/src/app.tsx/01M5AYXP2WM5WYKYA2RC58AAYC-each-capability-mirrors-a-ho.md b/.lane/memory/apps/app/src/app.tsx/01M5AYXP2WM5WYKYA2RC58AAYC-each-capability-mirrors-a-ho.md new file mode 100644 index 00000000..320e63d2 --- /dev/null +++ b/.lane/memory/apps/app/src/app.tsx/01M5AYXP2WM5WYKYA2RC58AAYC-each-capability-mirrors-a-ho.md @@ -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 diff --git a/.lane/memory/packages/ui/src/components/session-item/session-item-actions.tsx/01M5AYXPASFB8MZPK7Y2G4B4B1-an-action-this-row-can-t-per.md b/.lane/memory/packages/ui/src/components/session-item/session-item-actions.tsx/01M5AYXPASFB8MZPK7Y2G4B4B1-an-action-this-row-can-t-per.md new file mode 100644 index 00000000..8b284ca8 --- /dev/null +++ b/.lane/memory/packages/ui/src/components/session-item/session-item-actions.tsx/01M5AYXPASFB8MZPK7Y2G4B4B1-an-action-this-row-can-t-per.md @@ -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 diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index 8276c608..bc32d1d5 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -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", @@ -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" diff --git a/packages/ui/src/components/session-item/session-item-actions.tsx b/packages/ui/src/components/session-item/session-item-actions.tsx index 6e3002d6..8d04f83f 100644 --- a/packages/ui/src/components/session-item/session-item-actions.tsx +++ b/packages/ui/src/components/session-item/session-item-actions.tsx @@ -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) { stop(event); @@ -219,7 +225,7 @@ export function RowMenu( } return ( - + )} - - - Rename channel - - - - Fork channel - - - - Channel info - - {data.member && ( - + {onRename && ( + + + Rename channel + + )} + {onFork && ( + + + Fork channel + + )} + {onInfo && ( + + + Channel info + + )} + {leave && ( + Leave channel @@ -302,14 +314,12 @@ export function RowMenu( )} - - - Archive channel - + {onArchive && ( + + + Archive channel + + )} {onDelete && ( @@ -321,26 +331,22 @@ export function RowMenu( )} {!lobby && archived && ( <> - - - Unarchive channel - - - - Delete channel - + {onArchive && ( + + + Unarchive channel + + )} + {onDelete && ( + + + Delete channel + + )} )} - {lobby && ( - + {lobby && onRebuild && ( + Rebuild Lobby diff --git a/packages/ui/src/components/session-item/session-item.tsx b/packages/ui/src/components/session-item/session-item.tsx index 884654bb..d6cdafc9 100644 --- a/packages/ui/src/components/session-item/session-item.tsx +++ b/packages/ui/src/components/session-item/session-item.tsx @@ -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, diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 3fe75ad6..60354f5e 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -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;