Skip to content
43 changes: 40 additions & 3 deletions apps/web/src/app/(app)/cloud/sessions/SessionsPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Search, Cloud, Terminal, Puzzle, Bot, Workflow } from 'lucide-react';
import { Search, Cloud, Terminal, Puzzle, Bot, Workflow, Loader2 } from 'lucide-react';
import { SetPageTitle } from '@/components/SetPageTitle';
import type { SessionsListItem } from '@/components/cloud-agent/SessionsList';
import { SessionsList } from '@/components/cloud-agent/SessionsList';
Expand All @@ -31,6 +31,7 @@ import Link from 'next/link';
import { PageContainer } from '@/components/layouts/PageContainer';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm';
import { useCloudSessionFork } from '@/components/cloud-agent-next/use-cloud-session-fork';

/** Platform filter options matching the badge logic in SessionsList */
const PLATFORM_OPTIONS: readonly {
Expand Down Expand Up @@ -59,7 +60,10 @@ export function SessionsPageContent() {
const [platformFilter, setPlatformFilter] = useState<PlatformFilterValue>('all');
const [sessionFilter, setSessionFilter] = useState<SessionFilterValue>('all');
const [pendingSessionIds, setPendingSessionIds] = useState(() => new Set<string>());
type SessionWithSource = SessionsListItem & { source: 'v2' };
type SessionWithSource = SessionsListItem & {
source: 'v2';
cloudAgentSessionId?: string | null;
};
const [selectedSession, setSelectedSession] = useState<SessionWithSource | null>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);

Expand All @@ -74,6 +78,8 @@ export function SessionsPageContent() {
// Determine if we're in an organization context
const organizationId = pathname.match(/^\/organizations\/([^/]+)/)?.[1];

const { forkSessionToNewCloudSession, forkingSessionId } = useCloudSessionFork(organizationId);

// When in organization context, OrganizationTrialWrapper already provides PageContainer
const shouldUsePageContainer = !organizationId;

Expand Down Expand Up @@ -183,6 +189,7 @@ export function SessionsPageContent() {
return {
createdAt: session.created_at,
createdOnPlatform: session.created_on_platform,
cloudAgentSessionId: session.cloud_agent_session_id,
prompt,
repository,
sessionId: session.session_id,
Expand All @@ -203,6 +210,14 @@ export function SessionsPageContent() {
setIsDialogOpen(true);
};

const handleForkToCloud = async () => {
if (!selectedSession) return;
const forked = await forkSessionToNewCloudSession(selectedSession.sessionId);
if (forked) {
setIsDialogOpen(false);
}
};

const content = (
<>
<SetPageTitle title="Sessions" />
Expand Down Expand Up @@ -352,9 +367,31 @@ export function SessionsPageContent() {
<div className="space-y-3">
<h3 className="text-sm font-medium">Fork Session</h3>
<p className="text-muted-foreground text-xs">
Fork this session to continue working on it in your editor or CLI
{selectedSession.cloudAgentSessionId
? 'Fork this session to continue working on it in your editor, CLI, or a new Cloud Agent session'
: 'Fork this session to continue working on it in your editor or CLI'}
</p>

{/* Fork into a new Cloud Agent session */}
{selectedSession.cloudAgentSessionId && (
<Button
type="button"
variant="outline"
className="w-full gap-2"
disabled={forkingSessionId === selectedSession.sessionId}
onClick={() => void handleForkToCloud()}
>
{forkingSessionId === selectedSession.sessionId ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Cloud className="h-4 w-4" />
)}
{forkingSessionId === selectedSession.sessionId
? 'Forking to a new Cloud Agent session...'
: 'Fork to a new Cloud Agent session'}
</Button>
)}

{/* Open in Editor */}
<div className="flex justify-center">
<OpenInEditorButton
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/cloud-agent-next/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export function ChatHeader({
kiloSessionId={kiloSessionId}
sessionTitle={sessionTitle}
repository={repository}
organizationId={organizationId}
/>
<div className="flex min-w-0 items-center gap-1">
{sandboxStatusEligible && (
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/cloud-agent-next/CloudChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1264,7 +1264,11 @@ export default function CloudChatPage({
<div hidden={!chatTabActive} className={chatTabActive ? '' : 'hidden'}>
{isReadOnly ? (
!isLoading && sessionIdFromParams && fetchedSessionData ? (
<SessionContinuationPanel sessionId={sessionIdFromParams} />
<SessionContinuationPanel
sessionId={sessionIdFromParams}
organizationId={organizationId}
canForkToCloud={fetchedSessionData.cloudAgentSessionId !== null}
/>
) : null
) : (
<>
Expand Down
36 changes: 34 additions & 2 deletions apps/web/src/components/cloud-agent-next/SessionActionsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Loader2, Copy, Check, Share2, GitFork } from 'lucide-react';
import { Loader2, Copy, Check, Share2, GitFork, Cloud } from 'lucide-react';
import { useRawTRPCClient } from '@/lib/trpc/utils';
import { toast } from 'sonner';
import { CopyableCommand } from '@/components/CopyableCommand';
import { OpenInEditorButton } from '@/app/share/[shareId]/open-in-editor-button';
import { useCloudSessionFork } from './use-cloud-session-fork';

type SessionActionsDialogProps = {
open: boolean;
Expand All @@ -22,6 +23,8 @@ type SessionActionsDialogProps = {
kiloSessionId?: string;
sessionTitle?: string;
repository?: string;
/** Organization context the dialog renders in; omitted for personal sessions. */
organizationId?: string;
};

export function SessionActionsDialog({
Expand All @@ -30,11 +33,14 @@ export function SessionActionsDialog({
kiloSessionId,
sessionTitle,
repository,
organizationId,
}: SessionActionsDialogProps) {
const [isSharing, setIsSharing] = useState(false);
const [shareUrl, setShareUrl] = useState<string | null>(null);
const [isCopied, setIsCopied] = useState(false);
const trpc = useRawTRPCClient();
const { forkSessionToNewCloudSession, forkingSessionId } = useCloudSessionFork(organizationId);
const isForkingToCloud = forkingSessionId === kiloSessionId;

const handleShare = async () => {
if (!kiloSessionId) {
Expand Down Expand Up @@ -82,6 +88,14 @@ export function SessionActionsDialog({
}, 200);
};

const handleForkToCloud = async () => {
if (!kiloSessionId) return;
const forked = await forkSessionToNewCloudSession(kiloSessionId);
if (forked) {
handleClose();
}
};

const truncateId = (id: string, length: number = 8): string => {
if (id.length <= length) return id;
return `${id.slice(0, length)}...`;
Expand Down Expand Up @@ -183,11 +197,29 @@ export function SessionActionsDialog({
<h3 className="text-sm font-medium">Fork Session</h3>
</div>
<p className="text-muted-foreground text-xs">
Fork this session to continue working on it in your editor or CLI
Fork this session to continue working on it in your editor, CLI, or a new Cloud Agent
session
</p>

{kiloSessionId ? (
<div className="space-y-3">
<Button
type="button"
variant="outline"
className="w-full gap-2"
disabled={isForkingToCloud}
onClick={() => void handleForkToCloud()}
>
{isForkingToCloud ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Cloud className="h-4 w-4" />
)}
{isForkingToCloud
? 'Forking to a new Cloud Agent session...'
: 'Fork to a new Cloud Agent session'}
</Button>

<div className="flex justify-center">
<OpenInEditorButton
sessionId={kiloSessionId}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
'use client';

import { useState, useCallback } from 'react';
import { Copy, Check, Terminal, ChevronUp } from 'lucide-react';
import { Copy, Check, Terminal, ChevronUp, Cloud, Loader2 } from 'lucide-react';
import { OpenInEditorButton } from '@/app/share/[shareId]/open-in-editor-button';
import { Button } from '@/components/ui/button';
import { useCloudSessionFork } from './use-cloud-session-fork';

type SessionContinuationPanelProps = {
sessionId: string;
/** Organization context the panel renders in; omitted for personal sessions. */
organizationId?: string;
/** Whether the source session is a Cloud Agent session that can be forked. */
canForkToCloud?: boolean;
};

function SessionContinuationPanel({ sessionId }: SessionContinuationPanelProps) {
function SessionContinuationPanel({
sessionId,
organizationId,
canForkToCloud = false,
}: SessionContinuationPanelProps) {
const [copied, setCopied] = useState(false);
const [expanded, setExpanded] = useState(false);
const { forkSessionToNewCloudSession, forkingSessionId } = useCloudSessionFork(organizationId);

const cliCommand = `kilo --session ${sessionId} --cloud-fork`;
const isForking = forkingSessionId === sessionId;

const handleCopy = useCallback(() => {
void navigator.clipboard.writeText(cliCommand);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}, [cliCommand]);

const handleForkToCloud = useCallback(async () => {
const forked = await forkSessionToNewCloudSession(sessionId);
if (forked) {
setExpanded(false);
}
}, [forkSessionToNewCloudSession, sessionId]);

return (
<div className="border-border bg-muted/30 border-t">
<button
Expand All @@ -33,6 +52,25 @@ function SessionContinuationPanel({ sessionId }: SessionContinuationPanelProps)

{expanded && (
<div className="space-y-3 px-[max(1rem,calc(50%_-_27rem))] pb-4">
{canForkToCloud && (
<Button
type="button"
variant="outline"
className="w-full gap-2"
disabled={isForking}
onClick={() => void handleForkToCloud()}
>
{isForking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Cloud className="h-4 w-4" />
)}
{isForking
? 'Starting a new Cloud Agent session...'
: 'Continue in a new Cloud Agent session'}
</Button>
)}

<OpenInEditorButton sessionId={sessionId} pathOverride={`/s/${sessionId}`} />

<div className="space-y-1.5">
Expand All @@ -56,10 +94,6 @@ function SessionContinuationPanel({ sessionId }: SessionContinuationPanelProps)
</button>
</div>
</div>

<p className="text-muted-foreground text-xs italic">
Continue in Cloud Agent coming soon
</p>
</div>
)}
</div>
Expand Down
Loading