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: 2 additions & 0 deletions apps/api/src/services/ensure-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ async function ensurePostgres(home: string): Promise<ServiceInfo> {
user: PG_USER,
password: PG_PASSWORD,
persistent: true,
// Headroom for the API's many boot-time pools; default 100 storm-retries.
postgresFlags: ["-c", "max_connections=500"],
onLog: (msg: string) => {
if (process.env.DEBUG_SERVICES) console.log(`[pg] ${msg}`);
},
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import {
useVirtualMCP,
} from "@/sdk";
import { useSessionRuntime } from "@/hooks/use-session-runtime";
import {
draftsModeEnabled,
useIsOnProduction,
} from "@/components/thread/github/use-version-gate";
import { StartDraftCta } from "@/components/thread/github/start-draft-cta";
import { useNavigate } from "@tanstack/react-router";
import {
ArrowRight,
Expand Down Expand Up @@ -389,6 +394,10 @@ export function ChatInput({
const decopilotId = getWellKnownDecopilotVirtualMCP(org.id).id;
const selectedVm = useVirtualMCP(selectedVirtualMcp?.id);
const fastPreviewActive = useSessionRuntime(selectedVm?.id).runtime === "cms";
const isOnProduction = useIsOnProduction(
selectedVm,
taskCtx?.currentBranch ?? null,
);
const playSwitchSound = useSound(question004Sound);
const [connectionsOpen, setConnectionsOpen] = useState(false);
const { unsupportedFile, onUnsupportedFile, clearUnsupportedFile } =
Expand Down Expand Up @@ -630,6 +639,11 @@ export function ChatInput({
return <ChatInputDisabledState message={t("chat.input.readOnlyThread")} />;
}

// Production is the read-only live version — editing means branching off it.
if (draftsModeEnabled(selectedVm) && isOnProduction && taskCtx) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a web thread is pinned to an unsupported coding harness, this production branch returns the draft CTA before hostedRuntimeBlocked is checked. Check the hosted-runtime guard before offering the CTA so coding-agent threads cannot bypass the desktop-only restriction.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/chat/input.tsx, line 643:

<comment>When a web thread is pinned to an unsupported coding harness, this production branch returns the draft CTA before `hostedRuntimeBlocked` is checked. Check the hosted-runtime guard before offering the CTA so coding-agent threads cannot bypass the desktop-only restriction.</comment>

<file context>
@@ -630,6 +639,11 @@ export function ChatInput({
   }
 
+  // Production is the read-only live version — editing means branching off it.
+  if (draftsModeEnabled(selectedVm) && isOnProduction && taskCtx) {
+    return <StartDraftCta virtualMcpId={selectedVm?.id ?? ""} />;
+  }
</file context>
Suggested change
if (draftsModeEnabled(selectedVm) && isOnProduction && taskCtx) {
if (
!hostedRuntimeBlocked &&
draftsModeEnabled(selectedVm) &&
isOnProduction &&
taskCtx
) {

return <StartDraftCta virtualMcpId={selectedVm?.id ?? ""} />;
}

if (hostedRuntimeBlocked) {
return (
<ChatInputDisabledState
Expand Down
82 changes: 69 additions & 13 deletions apps/web/src/components/chat/pills/branch-pill.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,88 @@
import type { SandboxMap } from "@/sdk";
import { BranchPicker } from "../../thread/github/branch-picker";
import { BranchPickerLegacy } from "../../thread/github/branch-picker-legacy";

interface Props {
/** Draft & Releases mode: on → the releases switcher; off → the classic
* branch/PR picker. */
draftsMode: boolean;
virtualMcpId: string;
userLabel: string | null | undefined;
value: string | null | undefined;
onChange: (branch: string) => void;
onCreateBranch?: (branch: string) => void;
locked: boolean;
placement?: "chat" | "header";
/** Drafts-mode only: production branch shown as "Produção". */
baseBranch?: string | null;
/** Classic-picker only: repo scope for listing branches/PRs. */
orgId: string;
orgSlug: string;
userId: string;
userLabel: string | null | undefined;
virtualMcpId: string;
connectionId: string | null;
owner: string;
repo: string;
sandboxMap: SandboxMap | undefined;
value: string | null | undefined;
onChange: (branch: string) => void;
onCreateBranch?: (branch: string) => void;
locked: boolean;
placement?: "chat" | "header";
}

/** Thin wrapper over `BranchPicker`: a `locked` chat has a fixed branch, so any
* pick/create opens a new chat on it instead of switching (`spawnsNewChat`). */
export function BranchPill({ locked, placement, value, ...props }: Props) {
/** Routes to the drafts switcher or the classic branch/PR picker by the
* per-agent flag. A `locked` chat has a fixed branch, so any pick/create opens
* a new chat on it instead of switching (`spawnsNewChat`). */
export function BranchPill({
draftsMode,
locked,
placement,
value,
virtualMcpId,
userLabel,
baseBranch,
onChange,
onCreateBranch,
orgId,
orgSlug,
userId,
connectionId,
owner,
repo,
sandboxMap,
}: Props) {
if (draftsMode) {
return (
<BranchPicker
virtualMcpId={virtualMcpId}
userLabel={userLabel}
value={value}
baseBranch={baseBranch}
orgId={orgId}
orgSlug={orgSlug}
userId={userId}
connectionId={connectionId}
owner={owner}
repo={repo}
sandboxMap={sandboxMap}
onChange={onChange}
onCreateBranch={onCreateBranch}
spawnsNewChat={locked}
placement={placement}
/>
);
}
return (
<BranchPicker
{...props}
<BranchPickerLegacy
orgId={orgId}
orgSlug={orgSlug}
userId={userId}
userLabel={userLabel}
virtualMcpId={virtualMcpId}
connectionId={connectionId}
owner={owner}
repo={repo}
sandboxMap={sandboxMap}
value={value}
placement={placement}
onChange={onChange}
onCreateBranch={onCreateBranch}
spawnsNewChat={locked}
placement={placement}
/>
);
}
20 changes: 20 additions & 0 deletions apps/web/src/components/chat/pills/chat-mode-row.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ mock.module("../../thread/github/branch-picker", () => ({
),
}));

mock.module("../../thread/github/branch-picker-legacy", () => ({
BranchPickerLegacy: ({ spawnsNewChat }: { spawnsNewChat?: boolean }) => (
<button
type="button"
data-testid="legacy"
data-spawns-new-chat={spawnsNewChat ? "true" : "false"}
>
branch-picker-legacy
</button>
),
}));

import { ChatModeRowPure } from "./chat-mode-row";
import { BranchPill } from "./branch-pill";

Expand Down Expand Up @@ -51,6 +63,7 @@ describe("ChatModeRowPure", () => {
});

const BRANCH_PILL_PROPS = {
draftsMode: true,
orgId: "org-1",
orgSlug: "my-org",
userId: "user-1",
Expand Down Expand Up @@ -81,4 +94,11 @@ describe("BranchPill", () => {
"false",
);
});

it("renders the classic picker when draftsMode is off", () => {
const { getByTestId } = renderWithQueryClient(
<BranchPill {...BRANCH_PILL_PROPS} draftsMode={false} locked={false} />,
);
expect(getByTestId("legacy")).toBeInTheDocument();
});
});
53 changes: 30 additions & 23 deletions apps/web/src/components/chat/pills/chat-mode-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import type { ReactNode } from "react";
import type { VirtualMCPEntity } from "@decocms/shared/sdk/types";
import { useOptionalChatStream, useOptionalChatTask } from "../context";
import { BranchPill } from "./branch-pill";
import {
draftsModeEnabled,
useBaseBranch,
} from "../../thread/github/use-version-gate";
import { getActiveGithubRepo } from "@/lib/github-repo";
import { useProjectContext } from "@/sdk";
import {
defaultThreadRuntime,
readThreadRuntime,
} from "@decocms/shared/thread/session-runtime";
import { useProjectContext } from "@/sdk";
import { authClient } from "@/lib/auth-client";
import { branchUserLabel } from "@decocms/shared/branch-name";

Expand Down Expand Up @@ -62,42 +66,45 @@ export function ChatModeRow({ virtualMcp, currentBranch }: SmartProps) {
const connectionId = githubRepo?.connectionId;

const { data: session } = authClient.useSession();
const userId = session?.user?.id ?? "";
const userLabel = branchUserLabel(session?.user);
const userId = session?.user?.id ?? "";
const { org } = useProjectContext();

// Production branch shown as "Produção"; one shared source with the gate.
const baseBranch = useBaseBranch(virtualMcp, currentBranch);
const draftsMode = draftsModeEnabled(virtualMcp);

// Locked chat's branch is fixed: open a new chat on the picked branch.
const onChange = (next: string) => {
if (locked && createTask) createTask({ branch: next });
else if (setCurrentTaskBranch) void setCurrentTaskBranch(next);
};
// Locked or CMS→sandbox: branch off into a fresh thread, don't re-point.
const onCreateBranch = (next: string) => {
if ((locked || createBranchAsCms) && createTask)
createTask({ branch: next });
else if (setCurrentTaskBranch) void setCurrentTaskBranch(next);
};

const branchPill =
githubRepo && connectionId ? (
<BranchPill
// Remount on repo/connection change so search/tab/highlight state
// from the previous repo doesn't leak into the new one's picker.
// Remount per repo so the previous project's switcher state can't leak.
key={`${connectionId}:${githubRepo.owner}/${githubRepo.name}`}
draftsMode={draftsMode}
userLabel={userLabel}
virtualMcpId={virtualMcp?.id ?? ""}
value={currentBranch}
baseBranch={baseBranch}
orgId={org.id}
orgSlug={org.slug}
userId={userId}
userLabel={userLabel}
virtualMcpId={virtualMcp?.id ?? ""}
connectionId={connectionId}
owner={githubRepo.owner}
repo={githubRepo.name}
sandboxMap={virtualMcp?.metadata?.sandboxMap}
value={currentBranch}
onChange={(next) => {
// Locked chat's branch is fixed: open a new chat on the picked branch.
if (locked && createTask) {
createTask({ branch: next });
} else if (setCurrentTaskBranch) {
void setCurrentTaskBranch(next);
}
}}
onCreateBranch={(next) => {
// Locked or CMS→sandbox: branch off into a fresh thread, don't re-point.
if ((locked || createBranchAsCms) && createTask) {
createTask({ branch: next });
} else if (setCurrentTaskBranch) {
void setCurrentTaskBranch(next);
}
}}
onChange={onChange}
onCreateBranch={onCreateBranch}
locked={locked}
placement="chat"
/>
Expand Down
Loading
Loading