Skip to content
Merged
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
68 changes: 68 additions & 0 deletions ui/src/components/acp/sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,74 @@ describe('Sidebar', () => {
expect(agentHeaders[2]?.getAttribute('aria-label')).toBe('zulu conversations');
});

it('calls onNewConversation with focused instance name, not first alphabetical', async () => {
const onNewConversation = vi.fn();
const { default: userEvent } = await import('@testing-library/user-event');

renderWithProviders(
<SidebarWithFocus
agents={[
{
spritz: createSpritz('alpha'),
conversations: [createConversation('alpha-conv', 'Alpha conversation', 'alpha')],
},
{
spritz: createSpritz('beta'),
conversations: [createConversation('beta-conv', 'Beta conversation', 'beta')],
},
]}
selectedConversationId="beta-conv"
onSelectConversation={vi.fn()}
onNewConversation={onNewConversation}
collapsed={false}
onToggleCollapse={vi.fn()}
mobileOpen={false}
onCloseMobile={vi.fn()}
focusedSpritzName="beta"
/>,
);

// Click the top-level "New chat" button (pencil icon)
const newChatButton = screen.getByRole('button', { name: 'New chat' });
await userEvent.click(newChatButton);

// Should create for focused instance "beta", not alphabetically first "alpha"
expect(onNewConversation).toHaveBeenCalledWith('beta');
});

it('falls back to first agent for New chat when no instance is focused', async () => {
const onNewConversation = vi.fn();
const { default: userEvent } = await import('@testing-library/user-event');

renderWithProviders(
<Sidebar
agents={[
{
spritz: createSpritz('alpha'),
conversations: [createConversation('alpha-conv', 'Alpha conversation', 'alpha')],
},
{
spritz: createSpritz('beta'),
conversations: [createConversation('beta-conv', 'Beta conversation', 'beta')],
},
]}
selectedConversationId={null}
onSelectConversation={vi.fn()}
onNewConversation={onNewConversation}
collapsed={false}
onToggleCollapse={vi.fn()}
mobileOpen={false}
onCloseMobile={vi.fn()}
/>,
);

const newChatButton = screen.getByRole('button', { name: 'New chat' });
await userEvent.click(newChatButton);

// No focused instance, so falls back to first alphabetical ("alpha")
expect(onNewConversation).toHaveBeenCalledWith('alpha');
});

it('shows a selected optimistic provisioning conversation for a focused route before the agent is discoverable', () => {
renderWithProviders(
<SidebarWithFocus
Expand Down
9 changes: 5 additions & 4 deletions ui/src/components/acp/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function Sidebar({
a.spritz.metadata.name.localeCompare(b.spritz.metadata.name),
);
const firstAgentName = orderedAgents.length > 0 ? orderedAgents[0].spritz.metadata.name : null;
const newChatTargetName = focusedSpritzName || firstAgentName;
const focusMode = Boolean(focusedSpritzName);
const focusedAgentInList = Boolean(
focusedSpritzName && orderedAgents.some((group) => group.spritz.metadata.name === focusedSpritzName),
Expand Down Expand Up @@ -101,9 +102,9 @@ export function Sidebar({
<button
type="button"
aria-label="New chat"
disabled={!firstAgentName || creatingConversationFor === firstAgentName}
disabled={!newChatTargetName || creatingConversationFor === newChatTargetName}
className="flex size-9 items-center justify-center rounded-[var(--radius-lg)] text-foreground/70 transition-colors hover:bg-[var(--surface-emphasis)] hover:text-primary disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => { if (firstAgentName && creatingConversationFor !== firstAgentName) onNewConversation(firstAgentName); }}
onClick={() => { if (newChatTargetName && creatingConversationFor !== newChatTargetName) onNewConversation(newChatTargetName); }}
/>
}
>
Expand Down Expand Up @@ -146,10 +147,10 @@ export function Sidebar({
<nav aria-label="Sidebar navigation" className="flex shrink-0 flex-col gap-0.5">
<button
type="button"
disabled={!firstAgentName || creatingConversationFor === firstAgentName}
disabled={!newChatTargetName || creatingConversationFor === newChatTargetName}
className="flex w-full items-center gap-3 rounded-[var(--radius-lg)] px-3 py-2 text-[14px] text-foreground/80 transition-colors hover:bg-[var(--surface-emphasis)] hover:text-primary disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => {
if (firstAgentName && creatingConversationFor !== firstAgentName) onNewConversation(firstAgentName);
if (newChatTargetName && creatingConversationFor !== newChatTargetName) onNewConversation(newChatTargetName);
close();
}}
>
Expand Down
13 changes: 8 additions & 5 deletions ui/src/pages/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function getLatestConversation(conversations: ConversationInfo[]): ConversationI
return sortConversationsByRecency(conversations)[0] || null;
}


export function ChatPage() {
const { '*': splat } = useParams();
const splatSegments = (splat || '').split('/').filter(Boolean);
Expand All @@ -79,12 +80,14 @@ export function ChatPage() {
const composerRef = useRef<ComposerHandle>(null);
const selectedConversationRef = useRef<ConversationInfo | null>(null);
const autoCreatingConversationForRef = useRef<string | null>(null);
const creatingConversationForRef = useRef<string | null>(null);
const nameRef = useRef(name);
const urlConversationIdRef = useRef(urlConversationId);

nameRef.current = name;
urlConversationIdRef.current = urlConversationId;
selectedConversationRef.current = selectedConversation;
creatingConversationForRef.current = creatingConversationFor;

const selectedSpritzName = selectedConversation?.spec?.spritzName || name || '';
const selectedConversationId = selectedConversation?.metadata?.name || '';
Expand Down Expand Up @@ -191,7 +194,7 @@ export function ChatPage() {
currentGroup.spritz.metadata.name === currentName
? {
...currentGroup,
conversations: sortConversationsByRecency([...currentGroup.conversations, conv]),
conversations: [conv, ...currentGroup.conversations],
}
: currentGroup,
),
Expand Down Expand Up @@ -407,7 +410,7 @@ export function ChatPage() {
async (spritzName: string) => {
const normalizedSpritzName = String(spritzName || '').trim();
if (!normalizedSpritzName) return;
if (creatingConversationFor === normalizedSpritzName) {
if (creatingConversationForRef.current === normalizedSpritzName) {
return;
}
setCreatingConversationFor(normalizedSpritzName);
Expand All @@ -418,26 +421,26 @@ export function ChatPage() {
body: JSON.stringify({ spritzName: normalizedSpritzName }),
});
if (conv) {
navigate(chatConversationPath(normalizedSpritzName, conv.metadata.name), { replace: true });
setSelectedConversation(conv);
setAgents((currentGroups) =>
currentGroups.map((group) =>
group.spritz.metadata.name === normalizedSpritzName
? {
...group,
conversations: sortConversationsByRecency([...group.conversations, conv]),
conversations: [conv, ...group.conversations],
}
: group,
),
);
navigate(chatConversationPath(normalizedSpritzName, conv.metadata.name), { replace: true });
}
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to create conversation.');
} finally {
setCreatingConversationFor((current) => (current === normalizedSpritzName ? null : current));
}
},
[creatingConversationFor, navigate],
[navigate],
);

if (loading) {
Expand Down
Loading