Show optimistic chat for new workspaces

This commit is contained in:
Mohamed Boudra
2026-05-25 21:26:31 +07:00
parent d271597ee8
commit 30a8bffc24
11 changed files with 452 additions and 140 deletions
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { buildDraftPanelDescriptor } from "@/panels/draft-panel-descriptor";
function TestIcon() {
return null;
}
describe("buildDraftPanelDescriptor", () => {
it("uses the initial prompt title and running loader bucket during create", () => {
const descriptor = buildDraftPanelDescriptor({
isCreating: true,
pendingPrompt: "Build the dashboard",
icon: TestIcon,
});
expect(descriptor).toMatchObject({
label: "Build the dashboard",
subtitle: "Creating agent",
titleState: "ready",
statusBucket: "running",
});
});
it("falls back to the draft title for empty create prompts", () => {
const descriptor = buildDraftPanelDescriptor({
isCreating: true,
pendingPrompt: " ",
icon: TestIcon,
});
expect(descriptor.label).toBe("New Agent");
});
it("keeps ordinary draft tabs labeled as new agents", () => {
const descriptor = buildDraftPanelDescriptor({ isCreating: false, icon: TestIcon });
expect(descriptor).toMatchObject({
label: "New Agent",
subtitle: "New Agent",
titleState: "ready",
statusBucket: null,
});
});
});
+74 -1
View File
@@ -1,4 +1,5 @@
import type { DaemonClient } from "@server/client/daemon-client";
import { SquarePen } from "lucide-react-native";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import ReanimatedAnimated from "react-native-reanimated";
@@ -36,6 +37,7 @@ import { useArchiveAgent } from "@/hooks/use-archive-agent";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
import { buildDraftPanelDescriptor } from "@/panels/draft-panel-descriptor";
import {
type HostRuntimeConnectionStatus,
useHostRuntimeClient,
@@ -48,6 +50,7 @@ import {
deriveRouteBottomAnchorIntent,
deriveRouteBottomAnchorRequest,
} from "@/screens/agent/agent-ready-screen-bottom-anchor";
import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agent-tab";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys";
import { usePanelStore } from "@/stores/panel-store";
@@ -302,12 +305,82 @@ function AgentPanel() {
);
}
function DraftPanel() {
const {
serverId,
workspaceId,
tabId,
target,
openFileInWorkspace,
openImportSheet,
retargetCurrentTab,
} = usePaneContext();
const { isInteractive } = usePaneFocus();
invariant(target.kind === "draft", "DraftPanel requires draft target");
const handleCreated = useCallback(
(agentSnapshot: Parameters<typeof normalizeAgentSnapshot>[0]) => {
const normalized = normalizeAgentSnapshot(agentSnapshot, serverId);
useSessionStore.getState().setAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentSnapshot.id, normalized);
return next;
});
retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id });
},
[retargetCurrentTab, serverId],
);
return (
<WorkspaceDraftAgentTab
serverId={serverId}
workspaceId={workspaceId}
tabId={tabId}
draftId={target.draftId}
initialSetup={target.setup}
isPaneFocused={isInteractive}
onOpenWorkspaceFile={openFileInWorkspace}
onCreated={handleCreated}
onOpenImportSheet={openImportSheet}
/>
);
}
export function AgentConversationPanel() {
const { target } = usePaneContext();
if (target.kind === "draft") {
return <DraftPanel />;
}
if (target.kind === "agent") {
return <AgentPanel />;
}
invariant(false, "AgentConversationPanel requires an agent or draft target");
}
export const agentPanelRegistration: PanelRegistration<"agent"> = {
kind: "agent",
component: AgentPanel,
component: AgentConversationPanel,
useDescriptor: useAgentPanelDescriptor,
};
export function useDraftPanelDescriptor(
target: { kind: "draft"; draftId: string },
context: { serverId: string },
) {
const pendingCreate = useCreateFlowStore((state) => {
const pending = state.pendingByDraftId[target.draftId];
return pending?.serverId === context.serverId && pending.lifecycle === "active"
? pending
: null;
});
return buildDraftPanelDescriptor({
isCreating: Boolean(pendingCreate),
pendingPrompt: pendingCreate?.text,
icon: SquarePen,
});
}
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
const EMPTY_PENDING_PERMISSIONS = new Map<string, PendingPermission>();
const EMPTY_PENDING_PERMISSION_LIST: PendingPermission[] = [];
@@ -0,0 +1,28 @@
import type { ComponentType } from "react";
import type { PanelDescriptor, PanelIconProps } from "@/panels/panel-registry";
export function buildDraftPanelDescriptor(input: {
isCreating: boolean;
pendingPrompt?: string | null;
icon: ComponentType<PanelIconProps>;
}): PanelDescriptor {
const { icon, isCreating, pendingPrompt } = input;
const creatingLabel = pendingPrompt?.trim() || "New Agent";
if (isCreating) {
return {
label: creatingLabel,
subtitle: "Creating agent",
titleState: "ready",
icon,
statusBucket: "running",
};
}
return {
label: "New Agent",
subtitle: "New Agent",
titleState: "ready",
icon,
statusBucket: null,
};
}
+2 -59
View File
@@ -1,65 +1,8 @@
import { SquarePen } from "lucide-react-native";
import { useCallback } from "react";
import invariant from "tiny-invariant";
import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agent-tab";
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
import { AgentConversationPanel, useDraftPanelDescriptor } from "@/panels/agent-panel";
import type { PanelRegistration } from "@/panels/panel-registry";
import { useSessionStore } from "@/stores/session-store";
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
function useDraftPanelDescriptor() {
return {
label: "New Agent",
subtitle: "New Agent",
titleState: "ready" as const,
icon: SquarePen,
statusBucket: null,
};
}
function DraftPanel() {
const {
serverId,
workspaceId,
tabId,
target,
openFileInWorkspace,
openImportSheet,
retargetCurrentTab,
} = usePaneContext();
const { isInteractive } = usePaneFocus();
invariant(target.kind === "draft", "DraftPanel requires draft target");
const handleCreated = useCallback(
(agentSnapshot: Parameters<typeof normalizeAgentSnapshot>[0]) => {
const normalized = normalizeAgentSnapshot(agentSnapshot, serverId);
useSessionStore.getState().setAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentSnapshot.id, normalized);
return next;
});
retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id });
},
[retargetCurrentTab, serverId],
);
return (
<WorkspaceDraftAgentTab
serverId={serverId}
workspaceId={workspaceId}
tabId={tabId}
draftId={target.draftId}
initialSetup={target.setup}
isPaneFocused={isInteractive}
onOpenWorkspaceFile={openFileInWorkspace}
onCreated={handleCreated}
onOpenImportSheet={openImportSheet}
/>
);
}
export const draftPanelRegistration: PanelRegistration<"draft"> = {
kind: "draft",
component: DraftPanel,
component: AgentConversationPanel,
useDescriptor: useDraftPanelDescriptor,
};