+ {showFocusedProvisioningSection && focusedProvisioningSpritz && (
+
)}
{orderedAgents.length === 0 && !showFocusedProvisioningSection && (
@@ -216,16 +223,42 @@ export function Sidebar({
);
}
-function FocusedAgentProvisioningSection({ spritz }: { spritz: Spritz }) {
+function FocusedAgentProvisioningSection({
+ spritz,
+ selectedConversationId,
+}: {
+ spritz: Spritz;
+ selectedConversationId: string | null;
+}) {
const name = spritz.metadata.name;
- const statusLine = String(spritz.status?.message || '').trim()
- || [spritz.status?.phase, spritz.status?.acp?.state].filter(Boolean).join(' · ')
- || 'Preparing chat';
+ const statusLine = getProvisioningStatusLine(spritz);
+ const conversationLabel = describeChatAction(spritz).label;
+ const conversationSelected = !selectedConversationId;
return (
-
-
{name}
-
{statusLine}
+
+
+
+
+
+ {conversationLabel}
+
+
{statusLine}
+
);
}
diff --git a/ui/src/lib/provisioning.ts b/ui/src/lib/provisioning.ts
new file mode 100644
index 0000000..f1318c2
--- /dev/null
+++ b/ui/src/lib/provisioning.ts
@@ -0,0 +1,38 @@
+import type { Spritz } from '@/types/spritz';
+
+export const DEFAULT_PROVISIONING_MESSAGE = 'Creating your agent instance.';
+
+export function isSpritzChatReady(spritz: Spritz | null | undefined): boolean {
+ if (!spritz) return false;
+ return spritz.status?.phase === 'Ready' && spritz.status?.acp?.state === 'ready';
+}
+
+export function buildProvisioningPlaceholderSpritz(name: string): Spritz {
+ const normalizedName = String(name || '').trim();
+ return {
+ metadata: {
+ name: normalizedName,
+ namespace: '',
+ },
+ spec: {
+ image: '',
+ },
+ status: {
+ phase: 'Provisioning',
+ message: DEFAULT_PROVISIONING_MESSAGE,
+ acp: {
+ state: 'starting',
+ },
+ },
+ };
+}
+
+export function getProvisioningStatusLine(spritz: Spritz | null | undefined): string {
+ const message = String(spritz?.status?.message || '').trim();
+ if (message) return message;
+ const composite = [spritz?.status?.phase, spritz?.status?.acp?.state]
+ .map((value) => String(value || '').trim())
+ .filter(Boolean)
+ .join(' · ');
+ return composite || DEFAULT_PROVISIONING_MESSAGE;
+}
diff --git a/ui/src/pages/chat.test.tsx b/ui/src/pages/chat.test.tsx
index c0ee6dc..8116b1f 100644
--- a/ui/src/pages/chat.test.tsx
+++ b/ui/src/pages/chat.test.tsx
@@ -704,6 +704,26 @@ describe('ChatPage draft persistence', () => {
expect(screen.queryByText('Select a conversation or create a new instance.')).toBeNull();
});
+ it('keeps the provisioning route visible while the spritz resource is not discoverable yet', async () => {
+ requestMock.mockImplementation((path: string) => {
+ if (path === '/spritzes') {
+ return Promise.resolve({ items: [] });
+ }
+ if (path === '/spritzes/zeno-fresh-ridge') {
+ return Promise.reject(new Error('Not found.'));
+ }
+ return Promise.resolve({});
+ });
+
+ renderChatPage('/c/zeno-fresh-ridge');
+
+ expect(await screen.findByText('Your agent is being created now')).toBeTruthy();
+ expect(screen.getByText('We will start a chat automatically as soon as it is ready.')).toBeTruthy();
+ expect(screen.getAllByText('Creating your agent instance.').length).toBeGreaterThan(0);
+ expect(screen.getByTestId('sidebar-focused-spritz').textContent).toBe('zeno-fresh-ridge');
+ expect(screen.queryByText('Select a conversation or create a new instance.')).toBeNull();
+ });
+
it('automatically creates and opens a conversation once a provisioning agent becomes ready', async () => {
const createdConversation = createConversation({
metadata: { name: 'conv-created' },
diff --git a/ui/src/pages/chat.tsx b/ui/src/pages/chat.tsx
index 2a4d27e..0ec97b5 100644
--- a/ui/src/pages/chat.tsx
+++ b/ui/src/pages/chat.tsx
@@ -8,6 +8,11 @@ import { useConfig } from '@/lib/config';
import { useChatConnection } from '@/lib/use-chat-connection';
import { readChatDraft, writeChatDraft, clearChatDraft } from '@/lib/chat-draft';
import { buildFallbackConversationTitle, hasDurableConversationTitle } from '@/lib/conversation-title';
+import {
+ buildProvisioningPlaceholderSpritz,
+ getProvisioningStatusLine,
+ isSpritzChatReady,
+} from '@/lib/provisioning';
import { chatConversationPath } from '@/lib/urls';
import { useNotice } from '@/components/notice-banner';
import { Sidebar } from '@/components/acp/sidebar';
@@ -29,11 +34,6 @@ interface AgentGroup {
const PROVISIONING_POLL_INTERVAL_MS = 2000;
-function isSpritzChatReady(spritz: Spritz | null | undefined): boolean {
- if (!spritz) return false;
- return spritz.status?.phase === 'Ready' && spritz.status?.acp?.state === 'ready';
-}
-
function getConversationActivityTime(conversation: ConversationInfo): number {
const raw = String(conversation.status?.lastActivityAt || '').trim();
if (!raw) return Number.NEGATIVE_INFINITY;
@@ -77,11 +77,12 @@ export function ChatPage() {
const selectedSpritzName = selectedConversation?.spec?.spritzName || name || '';
const selectedConversationId = selectedConversation?.metadata?.name || '';
const focusedSpritz = name
- ? spritzes.find((spritz) => spritz.metadata.name === name) || null
+ ? spritzes.find((spritz) => spritz.metadata.name === name) || buildProvisioningPlaceholderSpritz(name)
: null;
const provisioningSpritz = focusedSpritz && !isSpritzChatReady(focusedSpritz)
? focusedSpritz
: null;
+ const provisioningStatusLine = getProvisioningStatusLine(provisioningSpritz);
// Fetch agents and conversations
const fetchAgents = useCallback(async () => {
@@ -441,7 +442,7 @@ export function ChatPage() {
})()}
{!selectedConversation && provisioningSpritz && (
- {provisioningSpritz.status?.message || 'Preparing the agent chat...'}
+ {provisioningStatusLine}
)}
@@ -513,9 +514,9 @@ export function ChatPage() {
We will start a chat automatically as soon as it is ready.
- {provisioningSpritz.status?.message && (
+ {provisioningStatusLine && (
- {provisioningSpritz.status.message}
+ {provisioningStatusLine}
)}