Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apps/hosted/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ function getPort(): number {
return parseInt( process.env.STUDIO_WEB_SERVER_PORT ?? String( DEFAULT_PORT ), 10 );
}

// Star/archive live in the shared config (`~/.studio/shared.json`), not the
// session JSONL — the same store the desktop app reads, so flags set in either
// surface show up in both.
// The archived flag lives in the shared config (`~/.studio/shared.json`), not
// the session JSONL — the same store the desktop app reads, so flags set in
// either surface show up in both.
function hydrateAiSessionSummary(
summary: AiSessionSummary,
metadata?: Pick< AiSessionSummary, 'starred' | 'archived' >
metadata?: Pick< AiSessionSummary, 'archived' >
): AiSessionSummary {
return { ...summary, starred: metadata?.starred, archived: metadata?.archived };
return { ...summary, archived: metadata?.archived };
}

const root = getAiSessionsRootDirectory();
Expand Down Expand Up @@ -276,7 +276,7 @@ api.patch(
'/sessions/:id',
asyncHandler( async ( req: Request, res: Response ) => {
const { summary } = await loadAiSession( root, req.params.id );
const patch = req.body as { starred?: boolean; archived?: boolean };
const patch = req.body as { archived?: boolean };
// Same persistence the desktop app uses (updateAiSessionMetadata in
// ipc-handlers.ts): flags go to the shared config under its lock.
const metadata = await updateSharedSession( summary.id, patch );
Expand Down
2 changes: 1 addition & 1 deletion apps/local/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
'/sessions/:id',
asyncHandler( async ( req: Request, res: Response ) => {
const { summary } = await loadAiSession( sessionsRoot, req.params.id );
const patch = req.body as { starred?: boolean; archived?: boolean };
const patch = req.body as { archived?: boolean };
const [ metadata, placement ] = await Promise.all( [
updateSharedSession( summary.id, patch ),
readAiSessionPlacement( summary.id ),
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ export async function createAiSession(
export async function updateAiSessionMetadata(
_event: IpcMainInvokeEvent,
sessionIdOrPrefix: string,
patch: Pick< AiSessionSummary, 'starred' | 'archived' >
patch: Pick< AiSessionSummary, 'archived' >
): Promise< AiSessionSummary > {
const { summary } = await loadAiSessionFromStore( getSessionsDirectory(), sessionIdOrPrefix );
const [ metadata, placement ] = await Promise.all( [
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/data/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export interface Connector {
deleteSession( sessionId: string ): Promise< void >;
updateSessionMetadata(
sessionId: string,
patch: Pick< AiSessionSummary, 'starred' | 'archived' >
patch: Pick< AiSessionSummary, 'archived' >
): Promise< AiSessionSummary >;

// Create an empty session file so it appears immediately. When `siteId`
Expand Down
6 changes: 2 additions & 4 deletions apps/ui/src/data/queries/use-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ export function useCreateSession() {

function mergeSessionMetadata(
summary: AiSessionSummary,
patch: Pick< AiSessionSummary, 'starred' | 'archived' >
patch: Pick< AiSessionSummary, 'archived' >
): AiSessionSummary {
return {
...summary,
starred: patch.starred,
archived: patch.archived,
};
}
Expand All @@ -116,7 +115,7 @@ export function useUpdateSessionMetadata() {
Error,
{
sessionId: string;
patch: Pick< AiSessionSummary, 'starred' | 'archived' >;
patch: Pick< AiSessionSummary, 'archived' >;
},
{
previousSessions: AiSessionSummary[] | undefined;
Expand Down Expand Up @@ -171,7 +170,6 @@ export function useUpdateSessionMetadata() {
...current,
summary: {
...current.summary,
starred: summary.starred,
archived: summary.archived,
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,17 @@ describe( 'SessionChatActions', () => {
} );

it( 'archives a chat from the history row trailing action', async () => {
const onNewChat = vi.fn();
const onSwitchSession = vi.fn();

render(
<SessionChatActions
currentSessionId="current"
onNewChat={ vi.fn() }
onNewChat={ onNewChat }
onSwitchSession={ onSwitchSession }
sessions={ [
createSession( { id: 'current', firstPrompt: 'Current chat' } ),
createSession( {
id: 'older',
firstPrompt: 'Older chat',
starred: true,
} ),
createSession( { id: 'older', firstPrompt: 'Older chat' } ),
] }
/>
);
Expand All @@ -268,11 +265,39 @@ describe( 'SessionChatActions', () => {

expect( updateSessionMetadataMutate ).toHaveBeenCalledWith( {
sessionId: 'older',
patch: {
starred: true,
archived: true,
},
patch: { archived: true },
} );
expect( onNewChat ).not.toHaveBeenCalled();
expect( onSwitchSession ).not.toHaveBeenCalled();
} );

it( 'opens a new chat when archiving the current chat', async () => {
const onNewChat = vi.fn();
const onSwitchSession = vi.fn();

render(
<SessionChatActions
currentSessionId="current"
onNewChat={ onNewChat }
onSwitchSession={ onSwitchSession }
sessions={ [
createSession( { id: 'current', firstPrompt: 'Current chat' } ),
createSession( { id: 'older', firstPrompt: 'Older chat' } ),
] }
/>
);

fireEvent.click( screen.getByRole( 'button', { name: 'Chat history' } ) );
const currentItem = ( await screen.findByText( 'Current chat' ) ).closest< HTMLElement >(
'[role="menuitem"]'
);
fireEvent.click( within( currentItem! ).getByRole( 'button', { name: 'Archive chat' } ) );

expect( updateSessionMetadataMutate ).toHaveBeenCalledWith( {
sessionId: 'current',
patch: { archived: true },
} );
expect( onNewChat ).toHaveBeenCalled();
expect( onSwitchSession ).not.toHaveBeenCalled();
} );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ export function SessionChatActions( {
const archiveSession = ( session: AiSessionSummary ) => {
updateSessionMetadata.mutate( {
sessionId: session.id,
patch: {
starred: session.starred,
archived: true,
},
patch: { archived: true },
} );
// Archiving the chat you're on shouldn't leave you inside it.
if ( session.id === currentSessionId ) {
onNewChat();
}
};

useEffect( () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/common/ai/sessions/manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { AiSessionSummary, LoadedAiSession } from '@studio/common/ai/sessio

/**
* Session listing + creation, hydrated with the metadata that lives outside the
* session JSONL: star/archive flags (shared config) and site placement
* session JSONL: the archived flag (shared config) and site placement
* (app.json).
*/

Expand All @@ -25,11 +25,11 @@ export interface SessionSite {

export function hydrateAiSessionSummary(
summary: AiSessionSummary,
metadata?: Pick< AiSessionSummary, 'starred' | 'archived' >,
metadata?: Pick< AiSessionSummary, 'archived' >,
placement?: AiSessionSitePlacement
): AiSessionSummary {
return hydrateAiSessionSummaryWithPlacement(
{ ...summary, starred: metadata?.starred, archived: metadata?.archived },
{ ...summary, archived: metadata?.archived },
placement
);
}
Expand Down
1 change: 0 additions & 1 deletion packages/common/ai/sessions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { SessionEntry } from '@earendil-works/pi-coding-agent';
export type TurnStatus = 'success' | 'error' | 'max_turns' | 'interrupted';

export interface AiSessionMetadata {
starred?: boolean;
archived?: boolean;
}

Expand Down
4 changes: 0 additions & 4 deletions packages/common/lib/shared-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const SHARED_CONFIG_VERSION = 1;

export const sharedSessionMetadataSchema = z
.object( {
starred: z.boolean().optional(),
archived: z.boolean().optional(),
} )
.loose();
Expand Down Expand Up @@ -120,9 +119,6 @@ export async function updateSharedConfig( update: Partial< SharedConfig > ): Pro
}

function pruneSharedSessionMetadata( metadata: SharedSessionMetadata ): void {
if ( ! metadata.starred ) {
delete metadata.starred;
}
if ( ! metadata.archived ) {
delete metadata.archived;
}
Expand Down
19 changes: 8 additions & 11 deletions packages/common/lib/tests/shared-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,31 +204,31 @@ describe( 'Shared Config', () => {
JSON.stringify( {
version: 1,
sessions: {
abc123: { starred: true },
abc123: { archived: true },
},
} )
)
);

await expect( readSharedSessions() ).resolves.toEqual( {
abc123: { starred: true },
abc123: { archived: true },
} );
await expect( readSharedSession( 'abc123' ) ).resolves.toEqual( { starred: true } );
await expect( readSharedSession( 'abc123' ) ).resolves.toEqual( { archived: true } );
await expect( readSharedSession( 'missing' ) ).resolves.toBeUndefined();
} );

it( 'updates session metadata in place', async () => {
vi.mocked( readFile ).mockResolvedValue( Buffer.from( JSON.stringify( { version: 1 } ) ) );

await expect( updateSharedSession( 'abc123', { starred: true } ) ).resolves.toEqual( {
starred: true,
await expect( updateSharedSession( 'abc123', { archived: true } ) ).resolves.toEqual( {
archived: true,
} );

const written = vi.mocked( writeFile ).mock.calls[ 0 ][ 1 ] as string;
expect( JSON.parse( written ) ).toEqual( {
version: 1,
sessions: {
abc123: { starred: true },
abc123: { archived: true },
},
} );
} );
Expand All @@ -239,17 +239,14 @@ describe( 'Shared Config', () => {
JSON.stringify( {
version: 1,
sessions: {
abc123: { starred: true, archived: true },
abc123: { archived: true },
},
} )
)
);

await expect(
updateSharedSession( 'abc123', {
starred: false,
archived: undefined,
} )
updateSharedSession( 'abc123', { archived: undefined } )
).resolves.toBeUndefined();

const written = vi.mocked( writeFile ).mock.calls[ 0 ][ 1 ] as string;
Expand Down
Loading