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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ run-milvus-test.sh
# Cloudflare
.wrangler/
.env*.local
/.superset
93 changes: 67 additions & 26 deletions src/app/(app)/cloud/sessions/SessionsPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export function SessionsPageContent() {
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
const [platformFilter, setPlatformFilter] = useState<PlatformFilterValue>('all');
const [selectedSession, setSelectedSession] = useState<SessionsListItem | null>(null);
const [includeSubSessions, setIncludeSubSessions] = useState(false);
type SessionsListItemWithSource = SessionsListItem & { source: 'v1' | 'v2' };
const [selectedSession, setSelectedSession] = useState<SessionsListItemWithSource | null>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);

// Debounce search query (300ms delay)
Expand All @@ -73,22 +75,24 @@ export function SessionsPageContent() {
// Query for listing sessions (when not searching)
// Order by updated_at and filter by organization and platform
const { data: listData, isLoading: isListLoading } = useQuery(
trpc.cliSessions.list.queryOptions({
trpc.unifiedSessions.list.queryOptions({
limit: 50,
orderBy: 'updated_at',
organizationId: organizationId ?? null,
createdOnPlatform: platformFilter === 'all' ? undefined : platformFilter,
includeSubSessions,
})
);

// Query for searching sessions (uses debounced value)
const { data: searchData, isLoading: isSearchLoading } = useQuery({
...trpc.cliSessions.search.queryOptions({
...trpc.unifiedSessions.search.queryOptions({
search_string: debouncedSearchQuery.trim(),
limit: 50,
offset: 0,
organizationId: organizationId ?? null,
createdOnPlatform: platformFilter === 'all' ? undefined : platformFilter,
includeSubSessions,
}),
enabled: isSearching,
});
Expand All @@ -102,8 +106,9 @@ export function SessionsPageContent() {
updated_at: string;
created_on_platform: string;
cloud_agent_session_id: string | null;
}): SessionsListItem => {
const repository = extractRepoFromGitUrl(session.git_url) || 'Unknown repository';
source: 'v1' | 'v2';
}): SessionsListItemWithSource => {
const repository = extractRepoFromGitUrl(session.git_url) ?? null;
const prompt = session.title || 'Untitled';

return {
Expand All @@ -113,6 +118,7 @@ export function SessionsPageContent() {
repository,
sessionId: session.session_id,
mode: '',
source: session.source,
};
};

Expand All @@ -124,8 +130,11 @@ export function SessionsPageContent() {
const isLoading = isSearching ? isSearchLoading : isListLoading;

const handleSessionClick: SessionsListProps['onSessionClick'] = session => {
setSelectedSession(session);
setIsDialogOpen(true);
const enriched = sessions.find(s => s.sessionId === session.sessionId);
if (enriched) {
setSelectedSession(enriched);
setIsDialogOpen(true);
}
};

const content = (
Expand Down Expand Up @@ -165,6 +174,18 @@ export function SessionsPageContent() {
})}
</SelectContent>
</Select>
<Select
value={includeSubSessions ? 'all' : 'root'}
onValueChange={value => setIncludeSubSessions(value === 'all')}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="root">Root sessions</SelectItem>
<SelectItem value="all">All sessions</SelectItem>
</SelectContent>
</Select>
</div>
</div>

Expand Down Expand Up @@ -197,9 +218,11 @@ export function SessionsPageContent() {
<p className="text-sm">
<span className="font-medium">Session:</span> {selectedSession.prompt}
</p>
<p className="text-sm">
<span className="font-medium">Repository:</span> {selectedSession.repository}
</p>
{selectedSession.repository && (
<p className="text-sm">
<span className="font-medium">Repository:</span> {selectedSession.repository}
</p>
)}
<p className="text-sm">
<span className="font-medium">ID:</span> {selectedSession.sessionId}
</p>
Expand Down Expand Up @@ -230,24 +253,42 @@ export function SessionsPageContent() {
Fork this session to continue working on it in your editor or CLI
</p>

{/* Open in Editor */}
<div className="flex justify-center">
<OpenInEditorButton sessionId={selectedSession.sessionId} />
</div>
{selectedSession.source === 'v1' && (
<>
{/* Open in Editor (v1 only) */}
<div className="flex justify-center">
<OpenInEditorButton sessionId={selectedSession.sessionId} />
</div>

{/* Open in CLI (v1 only) */}
<div className="flex justify-center">
<OpenInCliButton command={`kilocode --fork ${selectedSession.sessionId}`} />
</div>

{/* Open in CLI */}
<div className="flex justify-center">
<OpenInCliButton command={`kilocode --fork ${selectedSession.sessionId}`} />
</div>
{/* Manual fork command (v1) */}
<div className="space-y-2">
<p className="text-muted-foreground text-xs">
Or use the fork command manually:
</p>
<CopyableCommand
command={`/session fork ${selectedSession.sessionId}`}
className="bg-muted rounded-md px-3 py-2 text-sm"
/>
</div>
</>
)}

{/* Manual fork command */}
<div className="space-y-2">
<p className="text-muted-foreground text-xs">Or use the fork command manually:</p>
<CopyableCommand
command={`/session fork ${selectedSession.sessionId}`}
className="bg-muted rounded-md px-3 py-2 text-sm"
/>
</div>
{selectedSession.source === 'v2' && (
<div className="space-y-2">
<p className="text-muted-foreground text-xs">
Use the CLI to fork this session:
</p>
<CopyableCommand
command={`kilo --session ${selectedSession.sessionId} --fork`}
className="bg-muted rounded-md px-3 py-2 text-sm"
/>
</div>
)}
</div>
</div>
)}
Expand Down
10 changes: 6 additions & 4 deletions src/components/cloud-agent-next/ChatSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,12 @@ export function ChatSidebar({
</div>
)}
</div>
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<GitBranch className="h-3 w-3" />
<span className="truncate">{session.repository}</span>
</div>
{session.repository && (
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<GitBranch className="h-3 w-3" />
<span className="truncate">{session.repository}</span>
</div>
)}
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<Clock className="h-3 w-3" />
<span>{timeAgo}</span>
Expand Down
74 changes: 70 additions & 4 deletions src/components/cloud-agent-next/CloudChatContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

'use client';

import { useEffect, useCallback, useState, useRef } from 'react';
import { useEffect, useCallback, useMemo, useState, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { useRouter, useSearchParams } from 'next/navigation';
import { toast } from 'sonner';
Expand Down Expand Up @@ -48,10 +48,11 @@ import { useResumeConfigModal } from './hooks/useResumeConfigModal';
import { useSessionConfigCommand } from './hooks/useSessionConfigCommand';
import { useOrgContextCommand } from './hooks/useOrgContextCommand';
import { usePreparedSession } from './hooks/usePreparedSession';
import { buildPrepareSessionRepoParams } from './utils/git-utils';
import { buildPrepareSessionRepoParams, extractRepoFromGitUrl } from './utils/git-utils';
import { useSlashCommandSets } from '@/hooks/useSlashCommandSets';
import { CloudChatPresentation } from './CloudChatPresentation';
import type { ResumeConfig } from './ResumeConfigModal';
import type { RepositoryOption } from '@/components/shared/RepositoryCombobox';
import type { AgentMode, SessionStartConfig } from './types';

type CloudChatContainerProps = {
Expand Down Expand Up @@ -135,7 +136,6 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps)
const {
showResumeModal,
pendingResumeSession,
pendingGitState,
streamResumeConfig,
reopenResumeModal,
handleResumeConfirm: handleResumeConfirmFromHook,
Expand Down Expand Up @@ -172,6 +172,67 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps)
// Fetch organization models
const { modelOptions, isLoadingModels, defaultModel } = useOrganizationModels(organizationId);

// Fetch GitHub repositories for resume config modal
const { data: githubRepoData, isLoading: isLoadingGitHubRepos } = useQuery(
organizationId
? trpc.organizations.cloudAgentNext.listGitHubRepositories.queryOptions({
organizationId,
forceRefresh: false,
})
: trpc.cloudAgentNext.listGitHubRepositories.queryOptions({
forceRefresh: false,
})
);

// Fetch GitLab repositories for resume config modal
const { data: gitlabRepoData, isLoading: isLoadingGitLabRepos } = useQuery(
organizationId
? trpc.organizations.cloudAgentNext.listGitLabRepositories.queryOptions({
organizationId,
forceRefresh: false,
})
: trpc.cloudAgentNext.listGitLabRepositories.queryOptions({
forceRefresh: false,
})
);

const isLoadingRepos = isLoadingGitHubRepos && isLoadingGitLabRepos;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Logic bug — && should be ||

With &&, isLoadingRepos is only true when both GitHub and GitLab queries are loading simultaneously. As soon as one finishes, the loading indicator disappears even though the other query is still in-flight, causing the repository list to appear incomplete.

Suggested change
const isLoadingRepos = isLoadingGitHubRepos && isLoadingGitLabRepos;
const isLoadingRepos = isLoadingGitHubRepos || isLoadingGitLabRepos;


// Combine repositories with platform tags for the resume config modal
const repositories = useMemo<RepositoryOption[]>(() => {
const github = (
(githubRepoData?.repositories || []) as Array<{
id: string | number;
fullName: string;
private?: boolean;
}>
).map(repo => ({
id: repo.id,
fullName: repo.fullName,
private: repo.private,
platform: 'github' as const,
}));
const gitlab = (
(gitlabRepoData?.repositories || []) as Array<{
id: string | number;
fullName: string;
private?: boolean;
}>
).map(repo => ({
id: repo.id,
fullName: repo.fullName,
private: repo.private,
platform: 'gitlab' as const,
}));
return [...github, ...gitlab];
}, [githubRepoData, gitlabRepoData]);

// Derive default repo from loaded session's git_url for the resume config modal
const defaultResumeRepo = useMemo(
() => (loadedDbSession?.git_url ? extractRepoFromGitUrl(loadedDbSession.git_url) : undefined),
[loadedDbSession?.git_url]
);

// Mobile sheet state
const [mobileSheetOpen, setMobileSheetOpen] = useState(false);

Expand Down Expand Up @@ -771,6 +832,7 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps)
...repoParams,
envVars: streamResumeConfig?.envVars,
setupCommands: streamResumeConfig?.setupCommands,
upstreamBranch: streamResumeConfig?.upstreamBranch,
});
} else {
result = await trpcClient.cloudAgentNext.prepareSession.mutate({
Expand All @@ -780,6 +842,7 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps)
...repoParams,
envVars: streamResumeConfig?.envVars,
setupCommands: streamResumeConfig?.setupCommands,
upstreamBranch: streamResumeConfig?.upstreamBranch,
});
}

Expand Down Expand Up @@ -815,6 +878,7 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps)
...repoParams,
envVars: streamResumeConfig?.envVars,
setupCommands: streamResumeConfig?.setupCommands,
upstreamBranch: streamResumeConfig?.upstreamBranch,
});
await initiateFromPreparedSession(effectiveSessionId);
return;
Expand Down Expand Up @@ -920,7 +984,9 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps)
showResumeModal={showResumeModal}
pendingSessionForOrgContext={pendingSessionForOrgContext}
pendingResumeSession={pendingResumeSession}
pendingGitState={pendingGitState}
repositories={repositories}
isLoadingRepos={isLoadingRepos}
defaultResumeRepo={defaultResumeRepo}
needsResumeConfig={needsResumeConfig}
resumeConfigPersisting={resumeConfigState.status === 'persisting'}
resumeConfigFailed={resumeConfigState.status === 'failed'}
Expand Down
22 changes: 14 additions & 8 deletions src/components/cloud-agent-next/CloudChatPresentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { AgentMode, SessionConfig, StoredSession, StoredMessage } from './t
import { isMessageStreaming } from './types';
import type { DbSessionDetails, IndexedDbSessionData } from './store/db-session-atoms';
import type { ModelOption } from '@/components/shared/ModelCombobox';
import type { RepositoryOption } from '@/components/shared/RepositoryCombobox';
import type { SlashCommand } from '@/lib/cloud-agent/slash-commands';

// V2: No conversion needed - StoredMessage format is used directly by MessageBubble
Expand Down Expand Up @@ -114,7 +115,12 @@ export type CloudChatPresentationProps = {
showResumeModal: boolean;
pendingSessionForOrgContext: IndexedDbSessionData | null;
pendingResumeSession: DbSessionDetails | null;
pendingGitState: { branch?: string } | null;
/** Available repositories for resume config modal */
repositories: RepositoryOption[];
/** Whether repositories are loading */
isLoadingRepos: boolean;
/** Default repo for resume config modal */
defaultResumeRepo?: string;

// Config state
needsResumeConfig: boolean;
Expand Down Expand Up @@ -199,7 +205,9 @@ export const CloudChatPresentation = memo(function CloudChatPresentation({
showResumeModal,
pendingSessionForOrgContext,
pendingResumeSession,
pendingGitState,
repositories,
isLoadingRepos,
defaultResumeRepo,
needsResumeConfig,
resumeConfigPersisting,
resumeConfigFailed,
Expand Down Expand Up @@ -257,12 +265,10 @@ export const CloudChatPresentation = memo(function CloudChatPresentation({
isOpen={showResumeModal}
onClose={onResumeClose}
onConfirm={onResumeConfirm}
session={{
session_id: pendingResumeSession.session_id,
git_url: pendingResumeSession.git_url ?? null,
title: pendingResumeSession.title,
}}
gitState={pendingGitState}
repositories={repositories}
isLoadingRepos={isLoadingRepos}
defaultRepo={defaultResumeRepo}
sessionTitle={pendingResumeSession.title}
modelOptions={modelOptions}
isLoadingModels={isLoadingModels}
defaultMode={
Expand Down
4 changes: 2 additions & 2 deletions src/components/cloud-agent-next/CloudNextSessionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ export function CloudNextSessionsPage({ organizationId }: CloudNextSessionsPageP

// Invalidate the sessions list cache so the sidebar shows the new session
void queryClient.invalidateQueries({
queryKey: trpc.cliSessions.list.queryKey({
queryKey: trpc.unifiedSessions.list.queryKey({
limit: 3,
createdOnPlatform: 'cloud-agent',
orderBy: 'updated_at',
Expand Down Expand Up @@ -449,7 +449,7 @@ export function CloudNextSessionsPage({ organizationId }: CloudNextSessionsPageP
selectedPlatform,
selectedRepo,
selectedProfile,
trpc.cliSessions.list,
trpc.unifiedSessions.list,
trpcClient,
]);

Expand Down
Loading