Skip to content
Closed
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
53 changes: 53 additions & 0 deletions .plans/session-list-v2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Session List V2 Migration

Switch the web cloud-agent session list from the unified session endpoint to the CLI session v2 endpoint, and ensure sessions are grouped by updated time.

## Background

All session list consumers currently call `unifiedSessions.list` — a tRPC procedure in `apps/web/src/routers/unified-sessions-router.ts` that runs a `UNION ALL` across both `cli_sessions` (v1) and `cli_sessions_v2` tables.

A dedicated `cliSessionsV2.list` procedure already exists in `apps/web/src/routers/cli-sessions-v2-router.ts:229` that queries only `cli_sessions_v2`.

For grouping, the sidebar in `ChatSidebar.tsx:49` already uses `session.updatedAt`, and all consumers pass `orderBy: 'updated_at'`. The schema default in `ListSessionsInputSchema` is still `'created_at'` though, which should be cleaned up.

---

## Task 1: Switch session list to `cliSessionsV2.list`

### Consumers to update

| File | Line | Usage |
| ---------------------------------------------------------------------- | ---- | ----------------------------------- |
| `apps/web/src/components/cloud-agent-next/hooks/useSidebarSessions.ts` | 89 | `unifiedSessions.list` query |
| `apps/web/src/app/(app)/cloud/sessions/SessionsPageContent.tsx` | 78 | `unifiedSessions.list` query |
| `apps/web/src/components/cloud-agent/CloudSessionsPage.tsx` | 471 | `unifiedSessions.list` query |
| `apps/web/src/components/cloud-agent-next/CloudChatPage.tsx` | 151 | `unifiedSessions.list` invalidation |
| `apps/web/src/components/cloud-agent-next/CloudSidebarLayout.tsx` | 138 | `unifiedSessions.list` invalidation |
| `apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx` | 653 | `unifiedSessions.list` invalidation |

### Steps

1. **Compare input/output schemas** between `unifiedSessions.list` and `cliSessionsV2.list` — identify field name or shape differences that consumers depend on (e.g., `source` field, column mapping, cursor format).
2. **Update each query consumer** to call `cliSessionsV2.list` instead of `unifiedSessions.list`.
3. **Update each invalidation consumer** to invalidate `cliSessionsV2.list` instead of `unifiedSessions.list`.
4. **Update search** — `unifiedSessions.search` is also a `UNION ALL`. Switch search callers to a v2-only equivalent. May need to add a `search` procedure to the v2 router if one doesn't exist.
5. **Update `recentRepositories`** — currently on the unified router, also a `UNION ALL`. Either add to v2 router or migrate.
6. **Typecheck** — run `pnpm typecheck` to catch schema mismatches from the migration.
7. **Consider deprecation** — decide whether to remove or keep the unified router for backward compatibility.

---

## Task 2: Group sessions by updated time

### Current state

- `ChatSidebar.tsx:49` — `groupSessionsByDate` already groups by `session.updatedAt`. This is correct.
- All consumers already pass `orderBy: 'updated_at'` to the list query.
- The `ListSessionsInputSchema` default for `orderBy` is `'created_at'` (unified-sessions-router.ts:40).

### Steps

1. **Audit all `createdAt`/`created_at` usage** in session-related ordering or display code.
2. **Change the default `orderBy`** in `ListSessionsInputSchema` from `'created_at'` to `'updated_at'` — and similarly in the v2 router input schema if applicable.
3. **Verify** `SessionsPageContent` and other full-page session views aren't sorting by `createdAt`.
4. **Verify** the v2 router's `list` procedure supports `orderBy: 'updated_at'` and uses it correctly.
79 changes: 24 additions & 55 deletions apps/web/src/app/(app)/cloud/sessions/SessionsPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { OpenInEditorButton } from '@/app/share/[shareId]/open-in-editor-button';
import { OpenInCliButton } from '@/app/share/[shareId]/open-in-cli-button';
import { CopyableCommand } from '@/components/CopyableCommand';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
Expand Down Expand Up @@ -53,7 +52,7 @@ export function SessionsPageContent() {
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
const [platformFilter, setPlatformFilter] = useState<PlatformFilterValue>('all');
const [includeSubSessions, setIncludeSubSessions] = useState(false);
type SessionWithSource = SessionsListItem & { source: 'v1' | 'v2' };
type SessionWithSource = SessionsListItem & { source: 'v2' };
const [selectedSession, setSelectedSession] = useState<SessionWithSource | null>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);

Expand All @@ -76,7 +75,7 @@ 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.unifiedSessions.list.queryOptions({
trpc.cliSessionsV2.list.queryOptions({
limit: 50,
orderBy: 'updated_at',
organizationId: organizationId ?? null,
Expand All @@ -86,13 +85,13 @@ export function SessionsPageContent() {
: platformFilter === 'cloud-agent'
? ['cloud-agent', 'cloud-agent-web']
: platformFilter,
includeSubSessions,
includeChildren: includeSubSessions,
})
);

// Query for searching sessions (uses debounced value)
const { data: searchData, isLoading: isSearchLoading } = useQuery({
...trpc.unifiedSessions.search.queryOptions({
...trpc.cliSessionsV2.search.queryOptions({
search_string: debouncedSearchQuery.trim(),
limit: 50,
offset: 0,
Expand All @@ -103,21 +102,20 @@ export function SessionsPageContent() {
: platformFilter === 'cloud-agent'
? ['cloud-agent', 'cloud-agent-web']
: platformFilter,
includeSubSessions,
includeChildren: includeSubSessions,
}),
enabled: isSearching,
});

// Convert API session to StoredSession format
const convertToStoredSession = (session: {
session_id: string;
title: string;
title: string | null;
git_url: string | null;
created_at: string;
updated_at: string;
created_on_platform: string;
cloud_agent_session_id: string | null;
source: 'v1' | 'v2';
}): SessionWithSource => {
const repository = extractRepoFromGitUrl(session.git_url) ?? null;
const prompt = session.title || 'Untitled';
Expand All @@ -129,7 +127,7 @@ export function SessionsPageContent() {
repository,
sessionId: session.session_id,
mode: '',
source: session.source,
source: 'v2' as const,
};
};

Expand Down Expand Up @@ -261,53 +259,24 @@ export function SessionsPageContent() {
Fork this session to continue working on it in your editor or CLI
</p>

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

{/* Open in CLI (v1 only) */}
<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>
</>
)}

{selectedSession.source === 'v2' && (
<>
{/* Open in Editor (v2) */}
<div className="flex justify-center">
<OpenInEditorButton
sessionId={selectedSession.sessionId}
pathOverride={`/s/${selectedSession.sessionId}`}
/>
</div>

{/* Fork in CLI (v2) */}
<div className="space-y-2">
<p className="text-muted-foreground text-xs">
Or use the CLI to fork this session:
</p>
<CopyableCommand
command={`kilo --session ${selectedSession.sessionId} --cloud-fork`}
className="bg-muted rounded-md px-3 py-2 text-sm"
/>
</div>
</>
)}
{/* Fork in CLI */}
<div className="space-y-2">
<p className="text-muted-foreground text-xs">
Or use the CLI to fork this session:
</p>
<CopyableCommand
command={`kilo --session ${selectedSession.sessionId} --cloud-fork`}
className="bg-muted rounded-md px-3 py-2 text-sm"
/>
</div>
</div>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/cloud-agent-next/CloudChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export default function CloudChatPage({ organizationId }: CloudChatPageProps) {
useEffect(() => {
if (prevActivityRef.current === 'busy' && activity.type === 'idle') {
playCelebrationSound();
void queryClient.invalidateQueries(trpc.unifiedSessions.list.pathFilter());
void queryClient.invalidateQueries(trpc.cliSessionsV2.list.pathFilter());
}
prevActivityRef.current = activity.type;
}, [activity.type, playCelebrationSound, queryClient, trpc]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
const trpc = useTRPC();

const { data: recentReposData } = useQuery({
...trpc.unifiedSessions.recentRepositories.queryOptions({
...trpc.cliSessionsV2.recentRepositories.queryOptions({
organizationId,
updatedSince: repoUpdatedSince,
}),
Expand Down Expand Up @@ -135,7 +135,7 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
toast.error('Failed to delete session');
}

void queryClient.invalidateQueries(trpc.unifiedSessions.list.pathFilter());
void queryClient.invalidateQueries(trpc.cliSessionsV2.list.pathFilter());
refetchSessions();
},
[
Expand All @@ -155,8 +155,8 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
async (sessionId: string, title: string) => {
await renameCliSessionV2({ session_id: sessionId, title });
renameSessionLocally(sessionId, title);
void queryClient.invalidateQueries(trpc.unifiedSessions.list.pathFilter());
void queryClient.invalidateQueries(trpc.unifiedSessions.search.pathFilter());
void queryClient.invalidateQueries(trpc.cliSessionsV2.list.pathFilter());
void queryClient.invalidateQueries(trpc.cliSessionsV2.search.pathFilter());
refetchSessions();
},
[renameCliSessionV2, renameSessionLocally, queryClient, trpc, refetchSessions]
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) {

const repoUpdatedSince = useMemo(() => startOfDay(subDays(new Date(), 5)).toISOString(), []);
const { data: recentRepoData } = useQuery(
trpc.unifiedSessions.recentRepositories.queryOptions({
trpc.cliSessionsV2.recentRepositories.queryOptions({
organizationId: organizationId ?? null,
updatedSince: repoUpdatedSince,
})
Expand Down Expand Up @@ -650,7 +650,7 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) {
}

void queryClient.invalidateQueries({
queryKey: trpc.unifiedSessions.list.queryKey({
queryKey: trpc.cliSessionsV2.list.queryKey({
limit: 3,
createdOnPlatform: 'cloud-agent',
orderBy: 'updated_at',
Expand Down Expand Up @@ -682,7 +682,7 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) {
selectedPlatform,
selectedRepo,
selectedProfile,
trpc.unifiedSessions.list,
trpc.cliSessionsV2.list,
trpcClient,
variant,
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Hook for managing sidebar session list
*
* Fetches sessions from the unified sessions router and maintains them in Jotai atoms
* Fetches sessions from the CLI sessions v2 router and maintains them in Jotai atoms
* for reactive updates across the UI. Supports search and platform filtering.
*/

Expand Down Expand Up @@ -43,13 +43,13 @@ function dbSessionToStoredSession(session: DbSession | DbSessionV2): StoredSessi
mode: v1?.last_mode ?? 'code',
model: v1?.last_model ?? '',
status: session.cloud_agent_session_id ? 'active' : 'completed',
createdAt: session.created_at.toISOString(),
updatedAt: session.updated_at.toISOString(),
createdAt: session.created_at,
updatedAt: session.updated_at,
messages: [],
cloudAgentSessionId: session.cloud_agent_session_id,
createdOnPlatform: v1?.created_on_platform ?? null,
sessionStatus: session.status,
sessionStatusUpdatedAt: session.status_updated_at?.toISOString() ?? null,
sessionStatusUpdatedAt: session.status_updated_at ?? null,
};
}

Expand Down Expand Up @@ -86,10 +86,10 @@ export function useSidebarSessions(options?: UseSidebarSessionsOptions): UseSide
createdOnPlatform,
gitUrl,
};
const listQueryKey = trpc.unifiedSessions.list.queryKey(listInput);
const listQueryKey = trpc.cliSessionsV2.list.queryKey(listInput);

const { data: listData, isLoading: isListLoading } = useQuery({
...trpc.unifiedSessions.list.queryOptions(listInput),
...trpc.cliSessionsV2.list.queryOptions(listInput),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

WARNING: Switching the sidebar to cliSessionsV2 drops legacy sessions

unifiedSessions.list was the only query here that still UNIONed cli_sessions and cli_sessions_v2. After this swap, any user whose history still lives in cli_sessions loses those sessions from the sidebar/search immediately, even though the PR keeps the unified router around for backward compatibility. Unless every legacy row has already been backfilled into cli_sessions_v2, this is a breaking regression.

staleTime: 5000,
enabled: !isSearchActive,
});
Expand All @@ -98,7 +98,7 @@ export function useSidebarSessions(options?: UseSidebarSessionsOptions): UseSide
const searchInput = { search_string: searchQuery, createdOnPlatform, organizationId, gitUrl };

const { data: searchData, isLoading: isSearchLoading } = useQuery({
...trpc.unifiedSessions.search.queryOptions(searchInput),
...trpc.cliSessionsV2.search.queryOptions(searchInput),
staleTime: 5000,
enabled: isSearchActive,
});
Expand Down Expand Up @@ -135,8 +135,8 @@ export function useSidebarSessions(options?: UseSidebarSessionsOptions): UseSide
repository: extractRepoDisplay(row.git_url),
branch: row.git_branch,
prompt: row.title || `Session ${row.session_id.substring(0, 8)}`,
mode: row.last_mode ?? 'code',
model: row.last_model ?? '',
mode: 'code',
model: '',
status: row.cloud_agent_session_id ? ('active' as const) : ('completed' as const),
createdAt: row.created_at,
updatedAt: row.updated_at,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@ describe('getSessionDisplayTitle', () => {
git_branch: null,
cloud_agent_session_id: null,
created_on_platform: 'unknown',
created_at: new Date(),
updated_at: new Date(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
last_mode: null,
last_model: null,
version: 0,
Expand Down
Loading
Loading