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
19 changes: 17 additions & 2 deletions apps/web/src/components/cloud-agent-next/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useEffect, useMemo, useRef, useState, type RefObject } from 'react';
import { useEffect, useMemo, useRef, useState, type MouseEvent, type RefObject } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import {
Expand All @@ -16,6 +16,7 @@ import type { SessionCostBreakdown } from './session-cost-breakdown';
import { SessionActionsDialog } from './SessionActionsDialog';
import { SoundToggleButton } from '@/components/shared/SoundToggleButton';
import { FeedbackDialog } from './FeedbackDialog';
import { WorktreeChangesButton } from './WorktreeChanges';
import { buildRepoBrowseUrl, detectGitPlatform } from './utils/git-utils';
import { useTRPC } from '@/lib/trpc/utils';
import { SandboxStatusIndicator } from './SandboxStatusIndicator';
Expand Down Expand Up @@ -44,6 +45,8 @@ type ChatHeaderProps = {
sessionInfoTriggerRef: RefObject<HTMLElement | null>;
soundEnabled?: boolean;
onToggleSound?: () => void;
changesOpen?: boolean;
onToggleChanges?: (event: MouseEvent<HTMLButtonElement>) => void;
sessionTitle?: string;
sessionActive: boolean;
sandboxStatusEligible?: boolean;
Expand All @@ -62,6 +65,8 @@ export function ChatHeader({
sessionInfoTriggerRef,
soundEnabled = true,
onToggleSound,
changesOpen = false,
onToggleChanges,
kiloSessionId,
organizationId,
sessionTitle,
Expand Down Expand Up @@ -129,12 +134,22 @@ export function ChatHeader({
<div className="flex min-w-0 items-center gap-1">
{sandboxStatusEligible && (
<SandboxStatusIndicator
key={`${organizationId ?? 'personal'}:${cloudAgentSessionId}`}
key={`sandbox-status:${organizationId ?? 'personal'}:${cloudAgentSessionId}`}
cloudAgentSessionId={cloudAgentSessionId}
organizationId={organizationId}
sessionActive={sessionActive}
/>
)}
{onToggleChanges && (
<WorktreeChangesButton
key={`worktree-changes:${organizationId ?? 'personal'}:${cloudAgentSessionId}`}
cloudAgentSessionId={cloudAgentSessionId}
organizationId={organizationId}
open={changesOpen}
onToggle={onToggleChanges}
sessionActive={sessionActive}
/>
)}
{onToggleSound && (
<SoundToggleButton enabled={soundEnabled} onToggle={onToggleSound} size="sm" />
)}
Expand Down
68 changes: 64 additions & 4 deletions apps/web/src/components/cloud-agent-next/CloudChatPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { useSearchParams } from 'next/navigation';
import { useMutation, useQueryClient } from '@tanstack/react-query';
Expand Down Expand Up @@ -53,6 +53,8 @@ import {
import { billingPayerPresentation } from './billing-payer-presentation';
import type { OrganizationRole } from '@/lib/organizations/organization-types';
import { CloudAgentWorkspaceTabs } from './CloudAgentWorkspaceTabs';
import { WorktreeChangesDrawer } from './WorktreeChanges';
import { canOpenWorktreeChanges } from './worktree-changes';
import {
CHAT_TAB_ID,
addTerminalTab,
Expand Down Expand Up @@ -163,6 +165,8 @@ export default function CloudChatPage({
const childSessionDrawerFocusTargetRef = useRef<HTMLElement | null>(null);
const [preparationDrawerAttemptId, setPreparationDrawerAttemptId] = useState<string | null>(null);
const preparationDrawerFocusTargetRef = useRef<HTMLElement | null>(null);
const [changesDrawerSessionId, setChangesDrawerSessionId] = useState<string | null>(null);
const changesDrawerFocusTargetRef = useRef<HTMLElement | null>(null);

// URL-driven session switching
const sessionIdFromParams = searchParams?.get('sessionId') ?? null;
Expand All @@ -171,6 +175,8 @@ export default function CloudChatPage({
setChildSessionStack([]);
preparationDrawerFocusTargetRef.current = null;
setPreparationDrawerAttemptId(null);
changesDrawerFocusTargetRef.current = null;
setChangesDrawerSessionId(null);
if (sessionIdFromParams) {
void manager.switchSession(sessionIdFromParams as KiloSessionId);
} else {
Expand Down Expand Up @@ -255,6 +261,12 @@ export default function CloudChatPage({
organizationId,
scope: workspaceTabScope,
});
const canOpenChanges =
sessionIdFromParams !== null &&
isCurrentSession &&
canOpenWorktreeChanges(sessionId, isReadOnly) &&
fetchedSessionData?.organizationId === (organizationId ?? null);
const changesDrawerOpen = canOpenChanges && changesDrawerSessionId === sessionId;

if (
resolvedWorkspaceScope.currentUserId !== currentUserId ||
Expand All @@ -280,6 +292,11 @@ export default function CloudChatPage({
}
}, [sessionIdFromParams]);

useEffect(() => {
changesDrawerFocusTargetRef.current = null;
setChangesDrawerSessionId(null);
}, [sessionId, currentUserId, organizationId, canOpenChanges]);

// -- Session models -------------------------------------------------------
const sessionModels = useSessionModels({
activeSessionType,
Expand Down Expand Up @@ -693,9 +710,10 @@ export default function CloudChatPage({
const activeElement = document.activeElement;
childSessionDrawerFocusTargetRef.current =
activeElement instanceof HTMLElement ? activeElement : null;
// The two drawers overlay the same chat pane — only one may be open.
preparationDrawerFocusTargetRef.current = null;
setPreparationDrawerAttemptId(null);
changesDrawerFocusTargetRef.current = null;
setChangesDrawerSessionId(null);
setChildSessionStack([entry]);
}, []);

Expand Down Expand Up @@ -723,9 +741,10 @@ export default function CloudChatPage({
const activeElement = document.activeElement;
preparationDrawerFocusTargetRef.current =
activeElement instanceof HTMLElement ? activeElement : null;
// The two drawers overlay the same chat pane — only one may be open.
childSessionDrawerFocusTargetRef.current = null;
setChildSessionStack([]);
changesDrawerFocusTargetRef.current = null;
setChangesDrawerSessionId(null);
setPreparationDrawerAttemptId(attemptId);
}, []);

Expand All @@ -741,6 +760,30 @@ export default function CloudChatPage({
focusTarget.focus();
}, []);

const handleToggleChanges = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
changesDrawerFocusTargetRef.current = event.currentTarget;
childSessionDrawerFocusTargetRef.current = null;
setChildSessionStack([]);
preparationDrawerFocusTargetRef.current = null;
setPreparationDrawerAttemptId(null);
setChangesDrawerSessionId(current => (current === sessionId ? null : sessionId));
},
[sessionId]
);

const handleChangesDrawerOpenChange = useCallback((open: boolean) => {
if (!open) setChangesDrawerSessionId(null);
}, []);

const handleChangesDrawerCloseAutoFocus = useCallback((event: Event) => {
const focusTarget = changesDrawerFocusTargetRef.current;
changesDrawerFocusTargetRef.current = null;
if (!focusTarget?.isConnected) return;
event.preventDefault();
focusTarget.focus();
}, []);

// Surface the session's custom agents plus the current visible profile
// agents to the chat picker. `runtimeAgents` are the agents active when the
// session was created; the profile list enriches those same agents with
Expand Down Expand Up @@ -950,6 +993,8 @@ export default function CloudChatPage({
sessionInfoTriggerRef={sessionInfoTriggerRef}
soundEnabled={soundEnabled}
onToggleSound={handleToggleSound}
changesOpen={changesDrawerOpen}
onToggleChanges={canOpenChanges ? handleToggleChanges : undefined}
sessionActive={isStreaming || activity.type === 'busy' || activity.type === 'retrying'}
sandboxStatusEligible={isSandboxStatusEligible({
currentUserId,
Expand Down Expand Up @@ -1051,7 +1096,11 @@ export default function CloudChatPage({
className="relative flex min-h-0 flex-1 flex-col"
>
<div
inert={childSessionStack.length > 0 || preparationDrawerAttemptId !== null}
inert={
childSessionStack.length > 0 ||
preparationDrawerAttemptId !== null ||
changesDrawerOpen
}
className="flex min-h-0 flex-1 flex-col"
>
<div className="relative min-h-0 flex-1">
Expand Down Expand Up @@ -1302,6 +1351,17 @@ export default function CloudChatPage({
onCloseAutoFocus={handlePreparationDrawerCloseAutoFocus}
portalContainer={childSessionDrawerContainer}
/>
{canOpenChanges && sessionId && (
<WorktreeChangesDrawer
key={`${organizationId ?? 'personal'}:${sessionId}`}
cloudAgentSessionId={sessionId}
organizationId={organizationId}
open={changesDrawerOpen}
onOpenChange={handleChangesDrawerOpenChange}
onCloseAutoFocus={handleChangesDrawerCloseAutoFocus}
portalContainer={childSessionDrawerContainer}
/>
)}
</div>
</SuggestionContextProvider>
</PermissionContextProvider>
Expand Down
Loading
Loading