Skip to content
Draft
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
58 changes: 43 additions & 15 deletions packages/components/src/components/chat/chat-landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,24 @@ interface ChatLandingProps {
* chat route only; mobile keeps its base-context model.
*/
onSelectionUrlSync?: (search: ChatLandingSearch) => void;
/**
* Draw no product hint band above the composer.
*
* The band has two states and a host may serve neither. `no-machine` resolves
* `download-client` outside Electron, so it tells a member on a hosted surface
* to install the desktop app, beside a Report-a-bug button that uploads to
* Lody and a link to Lody's Discord. `no-agent-config` offers "Go to Settings",
* which on a host that mounts no settings surface only flips an atom.
*
* Off by default, so every upstream call site keeps both states.
*/
hideProductHints?: boolean;
/**
* Drop the run-config menu's Agent Role row; see
* `SessionChatInputAreaProps.hideAgentRoles`, which is the same prop on the
* session composer.
*/
hideAgentRoles?: boolean;
resetDraftKey?: string;
resetDraftOnKeyChange?: boolean;
}
Expand Down Expand Up @@ -557,6 +575,8 @@ function WorkspaceChatLanding({
preSelectedProject,
preSelectedRepo,
onSelectionUrlSync,
hideProductHints = false,
hideAgentRoles = false,
resetDraftKey,
resetDraftOnKeyChange = true,
}: ChatLandingProps) {
Expand Down Expand Up @@ -3771,14 +3791,18 @@ function WorkspaceChatLanding({
onRecentRunConfigSelect={handleRecentRunConfigSelect}
modeOptions={modeOptions}
selectedModeId={selectedModeId}
agentRoles={{
items: composerAgentRoleItems,
selectedRoleId: activeAgentRole?.id ?? null,
onSelect: handleAgentRoleSelect,
onCreate: handleAgentRoleCreate,
onEdit: handleAgentRoleEdit,
machine: scopedMachineId ? (machines.get(scopedMachineId) ?? null) : null,
}}
agentRoles={
hideAgentRoles
? undefined
: {
items: composerAgentRoleItems,
selectedRoleId: activeAgentRole?.id ?? null,
onSelect: handleAgentRoleSelect,
onCreate: handleAgentRoleCreate,
onEdit: handleAgentRoleEdit,
machine: scopedMachineId ? (machines.get(scopedMachineId) ?? null) : null,
}
}
/>
{/* Permission is part of what a Role pins, so behind one it stops being
a separate control and is stated in the Role's own face instead. It
Expand Down Expand Up @@ -4077,12 +4101,16 @@ function WorkspaceChatLanding({
cliType: selectedConfig?.cliType,
agentType: selectedConfig?.agentType,
}}
agentRoles={{
items: composerAgentRoleItems,
selectedRoleId: activeAgentRole?.id ?? null,
onSelect: handleAgentRoleSelect,
onCreate: handleAgentRoleCreate,
}}
agentRoles={
hideAgentRoles
? undefined
: {
items: composerAgentRoleItems,
selectedRoleId: activeAgentRole?.id ?? null,
onSelect: handleAgentRoleSelect,
onCreate: handleAgentRoleCreate,
}
}
/>
</div>
<SessionUsagePopover
Expand Down Expand Up @@ -6538,7 +6566,7 @@ function WorkspaceChatLanding({
}}
submitLabel={t('chat.send')}
submittingLabel={t('chat.submitting')}
hintType={hintType}
hintType={hideProductHints ? null : hintType}
noMachineVariant={isElectron ? 'daemon-starting' : 'download-client'}
hintDownloadClientMessage={t('chat.cliHint.downloadClient')}
hintDownloadClientLabel={t('chat.cliHint.downloadClientButton')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,14 @@ export function getSessionChatInputAreaShellClassName({

export interface SessionChatInputAreaProps {
session: SessionMeta;
/**
* Drop the run-config menu's Agent Role row.
*
* For a host whose workspace catalog carries no Roles and no writer for one:
* with an empty list the row does not disappear, it renders a "New role"
* entry that opens an editor whose save has nowhere to land.
*/
hideAgentRoles?: boolean;
sessionLocalProjectRootPath: string | null;
isMachineRemoved: boolean;
isAgentBusy: boolean;
Expand Down Expand Up @@ -458,6 +466,7 @@ export const SessionChatInputArea = memo(
forwardRef<SessionChatInputAreaHandle, SessionChatInputAreaProps>(function SessionChatInputArea(
{
session,
hideAgentRoles = false,
sessionLocalProjectRootPath,
isMachineRemoved,
canStopAgent = false,
Expand Down Expand Up @@ -2165,7 +2174,7 @@ export const SessionChatInputArea = memo(
configOptionValues={configOptionValues}
onConfigOptionChange={onConfigOptionChange}
fallbackAgent={{ cliType: session.cliType, agentType: session.agentType }}
agentRoles={agentRolesProp}
agentRoles={hideAgentRoles ? undefined : agentRolesProp}
/>
) : null;
const desktopAgentMachineIds = useMemo(
Expand Down Expand Up @@ -2198,7 +2207,7 @@ export const SessionChatInputArea = memo(
onConfigOptionChange={onConfigOptionChange}
modeOptions={modeOptions}
selectedModeId={selectedModeId}
agentRoles={agentRolesProp}
agentRoles={hideAgentRoles ? undefined : agentRolesProp}
/>
{sessionAgentRolePinsPermissionMode ? null : (
<DesktopPermissionModeButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,7 @@ export function SessionHeaderMenu({
onRestore,
onDelete,
compact = false,
hideCloudMenuItems = false,
t,
}: {
session: SessionMeta;
Expand Down Expand Up @@ -1030,6 +1031,18 @@ export function SessionHeaderMenu({
onRestore?: () => void | Promise<void>;
onDelete?: () => void | Promise<void>;
compact?: boolean;
/**
* Drop the three rows that only mean something inside a Lody CLOUD workspace:
* "Change owner", "Share with team" and "Copy URL".
*
* A host that mounts this menu against a single-member local workspace has no
* second member to hand a Session to, serves sharing from its own chrome, and
* has no cloud address for "Copy URL" to build — that last one copies a deep
* link from the daemon slug and toasts success either way, which is the worst
* of the three. Each row's existing gate (`owner`, `sharing`, `onCopyUrl`)
* answers a different question and none of them answers this one.
*/
hideCloudMenuItems?: boolean;
t: SessionSharingTranslator;
}) {
const isArchived = !!session.isArchived;
Expand Down Expand Up @@ -1340,7 +1353,7 @@ export function SessionHeaderMenu({
</DropdownMenuItem>
)}

{owner && !isArchived ? (
{owner && !isArchived && !hideCloudMenuItems ? (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<UserRoundCog className="h-3.5 w-3.5 shrink-0" />
Expand Down Expand Up @@ -1385,7 +1398,7 @@ export function SessionHeaderMenu({
{/* Copy URL stays in the Copy submenu even for private sessions (the
link still works for the owner); sharing is a separate action that
only appears while the conversation isn't team-visible. */}
{sharing && sharing.visibility !== 'team' ? (
{sharing && sharing.visibility !== 'team' && !hideCloudMenuItems ? (
<DropdownMenuItem
disabled={shareActionDisabled}
onClick={() => {
Expand Down Expand Up @@ -1470,14 +1483,16 @@ export function SessionHeaderMenu({
<Copy className="h-3.5 w-3.5 shrink-0" />
{t('sessions.copyAsMarkdown', 'Copy as Markdown')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
void onCopyUrl();
}}
>
<Copy className="h-3.5 w-3.5 shrink-0" />
{t('sessions.copyUrl', 'Copy URL')}
</DropdownMenuItem>
{hideCloudMenuItems ? null : (
<DropdownMenuItem
onClick={() => {
void onCopyUrl();
}}
>
<Copy className="h-3.5 w-3.5 shrink-0" />
{t('sessions.copyUrl', 'Copy URL')}
</DropdownMenuItem>
)}
</DropdownMenuSubContent>
</DropdownMenuSub>

Expand Down Expand Up @@ -1728,6 +1743,24 @@ interface SessionChatInterfaceProps {
subHeader?: React.ReactNode;
/** When true, hide the message area and input but keep the header and subHeader visible */
hideMessageArea?: boolean;
/** Forwarded to `SessionHeaderMenu`; see the prop's doc comment there. */
hideCloudMenuItems?: boolean;
/**
* Drop the notification permission prompt.
*
* For a host that mounts no push provider. The prompt's Enable button asks the
* browser for a permission nothing then consumes, and its "Don't remind me"
* writes a preference for a prompt that should not have appeared.
*/
hideNotificationPrompt?: boolean;
/**
* Drop the composer's Agent Role row.
*
* For a host whose workspace catalog carries no Roles and no way to write one:
* the row then renders its own empty state — a "New role" entry opening an
* editor whose save has nowhere to land — rather than disappearing.
*/
hideAgentRoles?: boolean;
/** When false, keep the local doc mounted without holding the remote room subscription. */
syncEnabled?: boolean;
/**
Expand Down Expand Up @@ -1897,6 +1930,9 @@ export const SessionChatInterface = memo(
titleSyncing,
subHeader,
hideMessageArea = false,
hideCloudMenuItems = false,
hideNotificationPrompt = false,
hideAgentRoles = false,
syncEnabled = !hideMessageArea,
isVisible = true,
isExternalHistoryRefreshing = false,
Expand Down Expand Up @@ -5568,6 +5604,7 @@ export const SessionChatInterface = memo(
}
}
onOpenReviewSettings={() => openSettings('preferences')}
hideCloudMenuItems={hideCloudMenuItems}
owner={ownerMenuState}
openedByRelations={openedByRelations}
onArchive={onArchiveSession}
Expand Down Expand Up @@ -5754,9 +5791,11 @@ export const SessionChatInterface = memo(
Visibility + enable/dismiss live inside NotificationPermissionPrompt (owned elsewhere)
and the actual grant/deny resolves on the settings page, so these must be emitted from
that component via onShown/onEnableClicked/onDismissed callbacks (see crossFileNeeds). */}
<NotificationPermissionPrompt
sessionCompleted={session.status?.type === 'idle' && !isSessionWorking}
/>
{hideNotificationPrompt ? null : (
<NotificationPermissionPrompt
sessionCompleted={session.status?.type === 'idle' && !isSessionWorking}
/>
)}

{/* An active auto-review run states itself here rather than
only in the "…" menu: the failure mode worth designing
Expand Down Expand Up @@ -5860,6 +5899,7 @@ export const SessionChatInterface = memo(
<SessionChatInputArea
ref={inputAreaRef}
session={session}
hideAgentRoles={hideAgentRoles}
sessionLocalProjectRootPath={resolvedLocalProjectMeta?.rootPath ?? null}
isMachineRemoved={isMachineRemoved}
isAgentBusy={isAgentBusy}
Expand Down
33 changes: 32 additions & 1 deletion packages/components/src/components/sessions/session-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -664,12 +664,35 @@ const SessionDetail = ({
urlPrNumber,
urlBrowser,
onMobileBack,
hideCloudMenuItems = false,
hideNotificationPrompt = false,
hideAgentRoles = false,
keyboardShortcutsAvailable = true,
}: {
sessionId: SessionId;
urlTab?: string;
urlPrNumber?: number;
urlBrowser?: boolean;
onMobileBack?: () => void;
/** Passed to every chat surface this page mounts; see
* `SessionChatInterfaceProps.hideCloudMenuItems`. */
hideCloudMenuItems?: boolean;
/** Passed to every chat surface this page mounts; see
* `SessionChatInterfaceProps.hideNotificationPrompt`. */
hideNotificationPrompt?: boolean;
/** Passed to every chat surface this page mounts; see
* `SessionChatInterfaceProps.hideAgentRoles`. */
hideAgentRoles?: boolean;
/**
* Whether the host answers keyboard commands at all.
*
* This page registers `session.focusInput`, and the composer reads that
* registration to draw a ⌘L discovery chip. A host that never calls
* `commands.attach(window)` has no dispatcher, so the chip advertises a chord
* that does nothing. On by default, so every existing call site keeps the
* registration and the chip.
*/
keyboardShortcutsAvailable?: boolean;
}) => {
const { t } = useTranslation();
const router = useRouter();
Expand Down Expand Up @@ -3709,14 +3732,19 @@ const SessionDetail = ({
run: handleOpenSearch,
});

// The composer draws its ⌘L discovery chip from this registration
// (`chat-composer.tsx` reads `commands.getKeybindingsFor('session.focusInput')`),
// so a host with no keyboard dispatcher must not make it. Passing the flag as
// `useCommand`'s second argument keeps this to one changed line in a file the
// seam pin reads line by line.
useCommand({
id: 'session.focusInput',
title: t('commands.session.focusInput', 'Focus Current Input'),
category: 'Editor',
keybindings: getCommandKeybindings('session.focusInput'),
when: () => Boolean(chatRefsMap.current.get(activeTabSessionId)),
run: handleFocusActiveInput,
});
}, keyboardShortcutsAvailable);

useCommand({
id: 'session.saveCurrentFile',
Expand Down Expand Up @@ -5550,6 +5578,9 @@ const SessionDetail = ({
workspaceSession: activeSession,
className: 'h-full',
hideHeader: true,
hideCloudMenuItems,
hideNotificationPrompt,
hideAgentRoles,
syncEnabled: isActive || pendingForkSourceId !== undefined,
isVisible,
onFileDiffClick: handleOpenFileDiffForChat,
Expand Down