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
89 changes: 63 additions & 26 deletions apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ import { CLOUD_AGENT_NEXT_WS_URL, SESSION_INGEST_WS_URL } from '@/lib/constants'
import { normalizeAlias } from './session-config';
import { usePostHog } from 'posthog-js/react';
import { fetchWebSessionSnapshotPage } from './session-page-adapter';
import type { CloudAgentApi } from '@kilocode/cloud-agent-sdk/transport';
import { cloudAgentWorktreeIdSchema } from '@kilocode/session-ingest-contracts';
import { createWorktreeReviewSend, type WorktreeReviewSendApi } from './worktree-review-send';

const ManagerContext = createContext<SessionManager | null>(null);
const UserWebConnectionContext = createContext<UserWebConnection | null>(null);
const CloudAgentContext = createContext<WorktreeReviewSendApi | null>(null);

type CloudAgentProviderProps = {
children: ReactNode;
Expand Down Expand Up @@ -82,6 +86,53 @@ export function CloudAgentProvider({ children, organizationId }: CloudAgentProvi
// Create manager once per provider instance.
// trpcClient is stable (from context); organizationId is stable per provider mount.
const managerRef = useRef<SessionManager | null>(null);
const reviewSendRef = useRef<WorktreeReviewSendApi | null>(null);
const send = async (
input: Parameters<CloudAgentApi['send']>[0] & { expectedWorktreeId?: string }
) => {
const payload = {
cloudAgentSessionId: input.sessionId,
payload: input.payload,
autoCommit: true,
messageId: input.messageId,
attachments: input.attachments ?? input.images,
expectedWorktreeId: input.expectedWorktreeId
? cloudAgentWorktreeIdSchema.parse(input.expectedWorktreeId)
: undefined,
};
return organizationId
? trpcClient.organizations.cloudAgentNext.sendMessage.mutate(
{ ...payload, organizationId },
{ context: { skipBatch: true } }
)
: trpcClient.cloudAgentNext.sendMessage.mutate(payload, { context: { skipBatch: true } });
};
if (reviewSendRef.current === null) {
reviewSendRef.current = createWorktreeReviewSend({
organizationId,
getSession: session_id => trpcClient.cliSessionsV2.getWithRuntimeState.query({ session_id }),
send: submission =>
send({
sessionId: submission.destinationCloudAgentSessionId,
payload: submission.payload,
messageId: submission.messageId,
expectedWorktreeId: submission.expectedWorktreeId,
}),
getMessageResult: submission => {
const input = {
cloudAgentSessionId: submission.destinationCloudAgentSessionId,
messageId: submission.messageId,
expectedWorktreeId: cloudAgentWorktreeIdSchema.parse(submission.expectedWorktreeId),
};
return organizationId
? trpcClient.organizations.cloudAgentNext.getMessageResult.query({
...input,
organizationId,
})
: trpcClient.cloudAgentNext.getMessageResult.query(input);
},
});
}
if (managerRef.current === null) {
managerRef.current = createSessionManager({
store: storeRef.current,
Expand Down Expand Up @@ -157,31 +208,7 @@ export function CloudAgentProvider({ children, organizationId }: CloudAgentProvi
lifecycleHooks: createBrowserLifecycleHooks(),

api: {
send: async input => {
if (organizationId) {
return trpcClient.organizations.cloudAgentNext.sendMessage.mutate(
{
cloudAgentSessionId: input.sessionId,
payload: input.payload,
autoCommit: true,
organizationId,
messageId: input.messageId,
attachments: input.attachments ?? input.images,
},
{ context: { skipBatch: true } }
);
}
return trpcClient.cloudAgentNext.sendMessage.mutate(
{
cloudAgentSessionId: input.sessionId,
payload: input.payload,
autoCommit: true,
messageId: input.messageId,
attachments: input.attachments ?? input.images,
},
{ context: { skipBatch: true } }
);
},
send,

interrupt: async payload => {
if (organizationId) {
Expand Down Expand Up @@ -362,12 +389,22 @@ export function CloudAgentProvider({ children, organizationId }: CloudAgentProvi
return (
<JotaiProvider store={storeRef.current}>
<UserWebConnectionContext.Provider value={sharedConnection}>
<ManagerContext.Provider value={managerRef.current}>{children}</ManagerContext.Provider>
<ManagerContext.Provider value={managerRef.current}>
<CloudAgentContext.Provider value={reviewSendRef.current}>
{children}
</CloudAgentContext.Provider>
</ManagerContext.Provider>
</UserWebConnectionContext.Provider>
</JotaiProvider>
);
}

export function useCloudAgent(): WorktreeReviewSendApi {
const api = useContext(CloudAgentContext);
if (!api) throw new Error('useCloudAgent must be used within CloudAgentProvider');
return api;
}

export function useOptionalManager(): SessionManager | null {
return useContext(ManagerContext);
}
Expand Down
99 changes: 98 additions & 1 deletion apps/web/src/components/cloud-agent-next/CloudChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Button } from '@/components/ui/button';
import { v4 as uuidv4 } from 'uuid';

import type { KiloSessionId } from '@kilocode/cloud-agent-sdk';
import { useManager } from './CloudAgentProvider';
import { useCloudAgent, useManager } from './CloudAgentProvider';
import { useWorktreeChatCreation, useWorktreeChatTabs } from './CloudSidebarLayout';
import { MobileSidebarToggle } from './MobileSidebarToggle';
import { ChatHeader } from './ChatHeader';
Expand Down Expand Up @@ -56,6 +56,9 @@ import { CloudAgentWorkspaceTabs } from './CloudAgentWorkspaceTabs';
import { WorktreeChangesDrawer } from './WorktreeChanges';
import { WorktreeFilePane } from './WorktreeFilePane';
import { commitsByMessageAnchor, isCommitSummaryRepresented } from './message-presentation';
import { WorktreeReviewDialog } from './WorktreeReviewDialog';
import { useWorktreeReview } from './useWorktreeReview';
import type { WorktreeReviewComment } from './worktree-review';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { canOpenWorktreeChanges } from './worktree-changes';
import {
Expand Down Expand Up @@ -146,6 +149,7 @@ export default function CloudChatPage({
organizationRole,
}: CloudChatPageProps) {
const manager = useManager();
const reviewApi = useCloudAgent();
const { createWorktreeChat, creatingWorktreeSourceSessionId } = useWorktreeChatCreation();
const {
selectedWorktreeId,
Expand Down Expand Up @@ -681,6 +685,96 @@ export default function CloudChatPage({
[canOpenChanges]
);

const handleReviewAccepted = useCallback(
(destinationKiloSessionId: string) => {
setWorkspaceTabs(state => selectWorkspaceTab(state, CHAT_TAB_ID));
openSession(destinationKiloSessionId);
},
[openSession]
);
const review = useWorktreeReview({
userId: currentUserId,
organizationId,
worktreeId: selectedWorktreeId,
activeKiloSessionId: sessionIdFromParams,
activeSessionConfig:
isCurrentSession &&
activeSessionType === 'cloud-agent' &&
sessionConfig?.sessionId === sessionId
? sessionConfig
: null,
enabled: Boolean(
currentUserId &&
selectedWorktreeId &&
(!sessionIdFromParams ||
(canOpenChanges &&
activeSessionType === 'cloud-agent' &&
fetchedSessionData?.worktreeId === selectedWorktreeId &&
!deletingSessionIds.includes(sessionIdFromParams)))
),
worktreeChats,
deletingSessionIds,
api: reviewApi,
onAccepted: handleReviewAccepted,
});
const reviewAgainFile = useRef<{
userId: string;
organizationId?: string;
workspaceScope: string;
kiloSessionId: string;
cloudAgentSessionId: string;
path: string;
} | null>(null);
const handleReviewAgain = (comment: WorktreeReviewComment) => {
if (!review.scope || review.locked) return;
const source = review.destinations.find(
destination =>
destination.cloudAgentSessionId === comment.anchor.capture.sourceCloudAgentSessionId
);
if (!source || !review.removeComment(comment.id)) return;
review.setOpen(false);
if (source.sessionId === sessionIdFromParams && canOpenChanges) {
setWorkspaceTabs(state =>
setFileTabMode(openFileTab(state, comment.anchor.path), comment.anchor.path, 'diff')
);
} else {
reviewAgainFile.current = {
...review.scope,
kiloSessionId: source.sessionId,
cloudAgentSessionId: source.cloudAgentSessionId,
path: comment.anchor.path,
};
openSession(source.sessionId);
}
};
useEffect(() => {
const file = reviewAgainFile.current;
if (!file) return;
if (
file.userId !== currentUserId ||
file.organizationId !== organizationId ||
file.workspaceScope !== workspaceTabScope
) {
reviewAgainFile.current = null;
return;
}
if (
!canOpenChanges ||
sessionIdFromParams !== file.kiloSessionId ||
sessionId !== file.cloudAgentSessionId
)
return;
reviewAgainFile.current = null;
setWorkspaceTabs(state => setFileTabMode(openFileTab(state, file.path), file.path, 'diff'));
}, [
canOpenChanges,
currentUserId,
organizationId,
sessionId,
sessionIdFromParams,
workspaceTabScope,
]);

const handleCloseFileTab = useCallback((path: string) => {
setWorkspaceTabs(state => closeFileTab(state, path));
}, []);
Expand Down Expand Up @@ -1152,6 +1246,7 @@ export default function CloudChatPage({
/>
)}
</div>
<WorktreeReviewDialog review={review} onReviewAgain={handleReviewAgain} />
{sessionIdFromParams && <div className="ml-auto shrink-0">{sessionActions}</div>}
</div>

Expand Down Expand Up @@ -1413,6 +1508,8 @@ export default function CloudChatPage({
organizationId={organizationId}
path={tab.path}
mode={tab.mode}
review={review.scope ? review.bindings : undefined}
reviewScope={review.scope ?? undefined}
onModeChange={mode =>
setWorkspaceTabs(state => setFileTabMode(state, tab.path, mode))
}
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/components/cloud-agent-next/WorktreeFilePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useWorktreeFile } from './useWorktreeFile';
import type { WorktreeFileViewMode } from './workspace-tabs';
import type { WorktreeReviewCapture } from './worktree-review';
import type { WorktreeFileReviewBindings } from './worktree-review-bindings';

const WorktreeFileRenderer = lazy(() => import('./WorktreeFileRenderer'));

Expand Down Expand Up @@ -88,19 +90,39 @@ export function WorktreeFilePane({
path,
mode,
onModeChange,
review,
reviewScope,
}: {
cloudAgentSessionId: string;
organizationId?: string;
path: string;
mode?: WorktreeFileViewMode;
onModeChange: (mode: WorktreeFileViewMode) => void;
review?: WorktreeFileReviewBindings;
reviewScope?: { userId: string; organizationId?: string; workspaceScope: string };
}) {
const { state, isFetching, reload } = useWorktreeFile({
cloudAgentSessionId,
organizationId,
path,
});
const statusProps = { path, isFetching, onReload: () => void reload() };
const reviewCapture: WorktreeReviewCapture | undefined =
review &&
reviewScope &&
reviewScope.organizationId === organizationId &&
cloudAgentSessionId.startsWith('workspace_') &&
'file' in state
? {
userId: reviewScope.userId,
organizationId: reviewScope.organizationId,
workspaceScope: reviewScope.workspaceScope,
sourceCloudAgentSessionId: cloudAgentSessionId,
revision: state.file.revision,
capturedAt: state.capturedAt,
comparison: state.comparison,
}
: undefined;

return (
<div
Expand Down Expand Up @@ -128,6 +150,8 @@ export function WorktreeFilePane({
onModeChange={onModeChange}
isFetching={isFetching}
onReload={statusProps.onReload}
review={reviewCapture ? review : undefined}
reviewCapture={reviewCapture}
/>
</Suspense>
</RendererBoundary>
Expand Down
Loading
Loading