Skip to content
Open
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
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