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
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler d1 migrations apply cyrus --local && wrangler dev",
"dev": "CI=1 wrangler d1 migrations apply cyrus --local && wrangler dev",
"check:types": "tsc --noEmit",
"db:push": "dotenvx run -- drizzle-kit push",
"db:generate": "dotenvx run -- drizzle-kit generate",
Expand Down
66 changes: 57 additions & 9 deletions apps/web/src/components/chat/composer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ import type { ComposerProps } from "@/types/composer";

export type { ComposerSubject } from "@/types/composer";

type ComposerBodyStatus =
| "loading"
| "agents-error"
| "unavailable"
| "settling"
| "interactive"
| "ready";

function getComposerBodyStatus(options: {
agentsLoading: boolean;
agentsError: boolean;
agentsReady: boolean;
hasAgents: boolean;
localDraft: boolean;
draftCatalogSettled: boolean;
isInteractivePending: boolean;
}): ComposerBodyStatus {
if (options.agentsLoading) return "loading";
if (options.agentsError) return "agents-error";
if (options.agentsReady && !options.hasAgents) return "unavailable";
if (options.localDraft && !options.draftCatalogSettled) return "settling";
if (options.isInteractivePending) return "interactive";
return "ready";
}

export function Composer({
projectId,
threadId,
Expand Down Expand Up @@ -60,15 +85,27 @@ export function Composer({
const canPasteUrls = supportsEmbeddedContext;
const composerBlocked = Boolean(threadError ?? catalog.catalogError);

// Drafts defer project-git until the user opens the branch chrome so opening
// a draft performs no worker RPCs beyond ambient listAgents.
const [draftGitOpen, setDraftGitOpen] = useState(false);
// Keep every draft lazy: a new draft/project identity must re-defer project
// git access until its own branch chrome is explicitly opened.
const [draftCatalogSettled, setDraftCatalogSettled] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: reset only on identity change
useEffect(() => {
setDraftGitOpen(false);
}, [threadId, projectId]);
// biome-ignore lint/correctness/useExhaustiveDependencies: reset only on identity change
useEffect(() => {
setDraftCatalogSettled(false);
}, [threadId, projectId]);
useEffect(() => {
if (draftCatalogSettled) return;
if (!localDraft) return;
if (!catalog.displayAgent || catalog.modelsLoading) return;
setDraftCatalogSettled(true);
}, [
draftCatalogSettled,
localDraft,
catalog.displayAgent,
catalog.modelsLoading,
]);
const threadGitStatus = useGitStatus(localDraft ? undefined : threadId);
const projectGitStatus = useProjectGitStatus(
localDraft && draftGitOpen ? projectId : undefined
Expand All @@ -87,7 +124,8 @@ export function Composer({
!agentsLoading &&
!agentsQuery.isError &&
!(agentsReady && !hasAgents) &&
!isInteractivePending;
!isInteractivePending &&
(!localDraft || draftCatalogSettled);

const editor = useComposerEditor({
threadId,
Expand All @@ -103,12 +141,22 @@ export function Composer({
onSend,
});

const composerBodyStatus = getComposerBodyStatus({
agentsLoading,
agentsError: agentsQuery.isError,
agentsReady,
draftCatalogSettled,
hasAgents,
isInteractivePending,
localDraft,
});

function renderComposerBody() {
if (agentsLoading) {
if (composerBodyStatus === "loading" || composerBodyStatus === "settling") {
return <ComposerSkeleton />;
}

if (agentsQuery.isError) {
if (composerBodyStatus === "agents-error") {
return (
<div className="group rounded-[22px] p-px transition-colors duration-200">
<div className="chat-composer-glass rounded-4xl border border-border px-4 py-5 text-center transition-colors duration-200">
Expand All @@ -123,11 +171,11 @@ export function Composer({
);
}

if (agentsReady && !hasAgents) {
if (composerBodyStatus === "unavailable") {
return <ComposerUnavailable />;
}

if (isInteractivePending) {
if (composerBodyStatus === "interactive") {
return (
<div className="space-y-2">
<ComposerQueueChips threadId={threadId} />
Expand Down
39 changes: 39 additions & 0 deletions shared/hooks/src/agent-catalog/selectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "vitest";
import {
type CatalogOption,
pickDisplayOption,
pickExplicitOption,
} from "./selectors";

const AGENTS: CatalogOption[] = [
{ id: "claude", name: "Claude" },
{ id: "codex", name: "Codex" },
];

describe("pickExplicitOption", () => {
test("returns empty string when no id is given", () => {
expect(pickExplicitOption(undefined, AGENTS)).toBe("");
});

test("returns empty string when the id is not in the options", () => {
expect(pickExplicitOption("missing", AGENTS)).toBe("");
});

test("returns the id when it matches an option", () => {
expect(pickExplicitOption("codex", AGENTS)).toBe("codex");
});
});

describe("pickDisplayOption", () => {
test("falls back to the first option when no id is given", () => {
expect(pickDisplayOption(undefined, AGENTS)).toBe("claude");
});

test("falls back to the first option when the id is not in the options", () => {
expect(pickDisplayOption("missing", AGENTS)).toBe("claude");
});

test("returns the id when it matches an option", () => {
expect(pickDisplayOption("codex", AGENTS)).toBe("codex");
});
});
10 changes: 8 additions & 2 deletions shared/hooks/src/agent-catalog/use-agent-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ export function useAgentCatalog({
pendingAgent ?? liveBinding?.agentName ?? thread?.agentName;

const boundAgent = pickExplicitOption(preferredAgent, agents);
// Drafts require an explicit agent selection before probing — do not fall
// back to agents[0], which would fire getDraftCatalog on open.
const displayAgent = isDraft
? pickExplicitOption(pendingAgent, agents)
: pickDisplayOption(preferredAgent, agents);
Expand Down Expand Up @@ -167,6 +165,14 @@ export function useAgentCatalog({
}),
});

useEffect(() => {
if (!isDraft) return;
if (pendingAgent) return;
const defaultAgent = agents[0]?.id;
if (!defaultAgent) return;
setPendingAgent(threadId, defaultAgent);
}, [agents, isDraft, pendingAgent, setPendingAgent, threadId]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

useEffect(() => {
if (!catalogAgent || models.length === 0) return;
const currentModelId =
Expand Down