Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions apps/extension/tests/e2e/agents-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ const buildDefaultCloudAgentStream = (): Record<string, unknown>[] => {
data: eventData,
eventId: ++_eventCounter,
executionId: 'exec-stream-1',
sessionId,
sessionId: DEFAULT_CLOUD_SESSION.cloudAgentSessionId,
streamEventType,
timestamp: new Date().toISOString(),
});
Expand Down Expand Up @@ -937,7 +937,7 @@ export const buildRunningCloudAgentStream = (
data: eventData,
eventId: ++_eventCounter,
executionId: 'exec-stream-1',
sessionId,
sessionId: DEFAULT_CLOUD_SESSION.cloudAgentSessionId,
streamEventType,
timestamp: new Date().toISOString(),
});
Expand Down Expand Up @@ -978,7 +978,7 @@ export const buildQuestionCloudAgentStream = (
data: eventData,
eventId: ++_eventCounter,
executionId: 'exec-stream-q',
sessionId,
sessionId: DEFAULT_CLOUD_SESSION.cloudAgentSessionId,
streamEventType,
timestamp: new Date().toISOString(),
});
Expand Down Expand Up @@ -1034,7 +1034,7 @@ export const buildPermissionCloudAgentStream = (
data: eventData,
eventId: ++_eventCounter,
executionId: 'exec-stream-p',
sessionId,
sessionId: DEFAULT_CLOUD_SESSION.cloudAgentSessionId,
streamEventType,
timestamp: new Date().toISOString(),
});
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/tests/e2e/agents-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ test('Agents session header reports context usage and session cost', async () =>
data,
eventId: ++eventCounter,
executionId: 'exec-usage',
sessionId,
sessionId: DEFAULT_CLOUD_SESSION.cloudAgentSessionId,
streamEventType,
timestamp: new Date().toISOString(),
});
Expand Down
18 changes: 16 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,21 @@ 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}
/>
)}
{onToggleSound && (
<SoundToggleButton enabled={soundEnabled} onToggle={onToggleSound} size="toolbar" />
)}
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