Skip to content
Merged
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
109 changes: 108 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,105 @@ 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 reviewCommentCounts = useMemo(() => {
const counts = new Map<string, number>();
if (!sessionId) return counts;
for (const comment of review.draft?.comments ?? []) {
if (comment.anchor.capture.sourceCloudAgentSessionId !== sessionId) continue;
counts.set(comment.anchor.path, (counts.get(comment.anchor.path) ?? 0) + 1);
Comment thread
eshurakov marked this conversation as resolved.
}
return counts;
}, [review.draft?.comments, sessionId]);
const reviewAgainFile = useRef<{
userId: string;
organizationId?: string;
workspaceScope: string;
kiloSessionId: string;
cloudAgentSessionId: string;
path: string;
} | null>(null);
const handleOpenReviewComment = (comment: WorktreeReviewComment) => {
if (!review.scope || review.locked) return;
const source = review.destinations.find(
destination =>
destination.cloudAgentSessionId === comment.anchor.capture.sourceCloudAgentSessionId
);
if (!source) 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 @@ -1157,6 +1260,7 @@ export default function CloudChatPage({
/>
)}
</div>
<WorktreeReviewDialog review={review} onOpenComment={handleOpenReviewComment} />
{sessionIdFromParams && <div className="ml-auto shrink-0">{sessionActions}</div>}
</div>

Expand Down Expand Up @@ -1418,6 +1522,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 Expand Up @@ -1460,6 +1566,7 @@ export default function CloudChatPage({
onCloseAutoFocus={handleChangesDrawerCloseAutoFocus}
onSelectFile={handleSelectWorktreeFile}
portalContainer={childSessionDrawerContainer}
commentCounts={reviewCommentCounts}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jest.mock('react-markdown', () => ({
React.createElement('p', null, children),
}));
jest.mock('remark-gfm', () => ({ __esModule: true, default: () => undefined }));
jest.mock('../../../node_modules/@pierre/diffs/dist/utils/iterateOverDiff.js', () => ({
iterateOverDiff: () => [],
}));

import { ConversationMessages } from './ConversationMessages';
import { commitsByMessageAnchor } from './message-presentation';
Expand Down
Loading
Loading