Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8506e7b
🤖 feat: add flow prompting workspace prompts
ammar-agent Mar 8, 2026
8089050
🤖 tests: add Flow Prompting UI coverage
ammar-agent Mar 8, 2026
5dd38b1
🤖 fix: preserve Flow Prompting rename and clear semantics
ammar-agent Mar 8, 2026
100fda8
🤖 fix: finalize Flow Prompting acceptance via persisted chat events
ammar-agent Mar 8, 2026
840ed59
🤖 fix: harden Flow Prompting acceptance and file IO
ammar-agent Mar 8, 2026
435da90
🤖 fix: trim Flow Prompting hot-path IO
ammar-agent Mar 8, 2026
113053d
🤖 fix: coalesce pending Flow Prompt clears
ammar-agent Mar 8, 2026
ea20277
🤖 fix: preserve queued turn semantics during Flow Prompting
ammar-agent Mar 8, 2026
367de87
🤖 tests: disable hanging Flow Prompting app harness suite
ammar-agent Mar 8, 2026
8d5516d
🤖 fix: guard Flow Prompting create and disable paths
ammar-agent Mar 8, 2026
7c93a6d
🤖 fix: drop stale Flow Prompting updates safely
ammar-agent Mar 8, 2026
f959024
🤖 ci: give integration tests more headroom
ammar-agent Mar 8, 2026
84a0271
🤖 fix: retry queued Flow Prompting sends safely
ammar-agent Mar 8, 2026
4ff16fd
🤖 fix: await fresh Flow Prompting state reads
ammar-agent Mar 8, 2026
fe193a9
🤖 fix: harden Flow Prompting error handling
ammar-agent Mar 8, 2026
5261f8e
🤖 fix: surface Flow Prompting migration failures
ammar-agent Mar 8, 2026
b1538da
🤖 fix: preserve successful forks on Flow Prompting copy errors
ammar-agent Mar 8, 2026
5280723
🤖 tests: move hanging Flow Prompting scenarios out of Jest
ammar-agent Mar 8, 2026
290b76f
🤖 fix: repair invalid Flow Prompting prompt paths
ammar-agent Mar 8, 2026
82baee5
🤖 fix: sync workspace agent selection for flow prompts
ammar-agent Mar 10, 2026
3c5e963
🤖 feat: polish flow prompting composer UX
ammar-agent Mar 10, 2026
90b3db0
🤖 feat: preview queued flow prompt updates
ammar-agent Mar 10, 2026
21a6711
🤖 feat: integrate flow prompt auto-send controls
ammar-agent Mar 10, 2026
6f73f6b
🤖 fix: serialize flow prompt follow-up metadata sync
ammar-agent Mar 10, 2026
6e7abb5
🤖 feat: tighten Flow Prompting composer layout
ammar-agent Mar 10, 2026
d21636b
🤖 fix: dedupe Flow Prompting dispatches
ammar-agent Mar 10, 2026
347b3b9
🤖 feat: compact the Flow Prompting composer
ammar-agent Mar 10, 2026
051e9e7
🤖 fix: align Flow Prompting accessory spacing
ammar-agent Mar 10, 2026
bde9ac9
🤖 fix: isolate Flow Prompting follow-up context
ammar-agent Mar 10, 2026
fe0bc51
🤖 feat: format Flow Prompting diff previews
ammar-agent Mar 10, 2026
f89a459
🤖 fix: flush queued agent sync updates
ammar-agent Mar 10, 2026
e84e7e8
🤖 fix: harden Flow Prompting preview and disable flow
ammar-agent Mar 10, 2026
7eb75bb
🤖 fix: preserve clear-update Flow Prompt controls
ammar-agent Mar 11, 2026
fc600c4
🤖 fix: clear stalled Flow Prompt retries on turn failure
ammar-agent Mar 11, 2026
5f97dd3
🤖 tests: restore AgentContext workspace sync harness
ammar-agent Mar 11, 2026
e0a5215
🤖 fix: tighten Flow Prompt preview refresh and composer controls
ammar-agent Mar 11, 2026
ecfc80c
🤖 fix: clamp compacted prompt attachments to budget
ammar-agent Mar 11, 2026
4581b92
🤖 fix: stabilize Flow Prompt composer layout
ammar-agent Mar 11, 2026
94d2777
🤖 fix: compact Flow Prompt composer controls
ammar-agent Mar 11, 2026
eec8e3c
🤖 fix: drop stale Flow Prompt retries before direct send
ammar-agent Mar 11, 2026
fb07d24
🤖 fix: widen Flow Prompt helper text
ammar-agent Mar 11, 2026
3573d1e
🤖 feat: add Agent Scope to Flow Prompt auto-send
ammar-agent Mar 11, 2026
8d18c01
🤖 fix: keep queued Flow Prompt clears pending
ammar-agent Mar 11, 2026
c708ada
🤖 fix: stub Flow Prompting in mock ORPC clients
ammar-agent Mar 11, 2026
f5e0adc
🤖 feat: steer flow prompts from Next heading
ammar-agent Mar 12, 2026
04787e6
🤖 tests: isolate AppLoader auth from App sidebar imports
ammar-agent Mar 12, 2026
b876f7f
🤖 tests: stub react-dnd in AppLoader auth tests
ammar-agent Mar 12, 2026
8311e80
🤖 tests: defer AppLoader and ProjectSidebar imports past mocks
ammar-agent Mar 12, 2026
4913a44
🤖 tests: reinstall DnD module mocks before each suite
ammar-agent Mar 12, 2026
0f18706
🤖 tests: satisfy lint for mock-before-load suites
ammar-agent Mar 12, 2026
efff987
🤖 fix: suppress automatic retries for failed flow prompts
ammar-agent Mar 12, 2026
b887ce1
🤖 fix: preserve backend workspace agents on first load
ammar-agent Mar 12, 2026
0efd6d9
🤖 tests: give send mode UI flow more CI headroom
ammar-agent Mar 12, 2026
5283d80
🤖 tests: give relative-path file editing more local timeout headroom
ammar-agent Mar 12, 2026
1e14a41
🤖 tests: update flow prompt retry helper signatures
ammar-agent Mar 12, 2026
62c72e7
🤖 fix: keep diff previews working with Next sections
ammar-agent Mar 13, 2026
8025bc3
🤖 fix: tolerate code fences inside Next previews
ammar-agent Mar 13, 2026
cb0049b
🤖 fix: derive safe fence lengths for flow prompt markdown blocks
ammar-agent Mar 13, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ jobs:
name: Test / Integration
needs: [changes]
if: ${{ (needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true') && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]') }}
timeout-minutes: 10
# Backend changes run the full tests/ tree (IPC + UI + provider-backed coverage),
# which can legitimately exceed 10 minutes on busy runners.
timeout-minutes: 15
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
Expand Down
152 changes: 91 additions & 61 deletions src/browser/components/AppLoader/AppLoader.auth.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "../../../../tests/ui/dom";

import React from "react";
import type { AppLoader as AppLoaderComponent } from "../AppLoader/AppLoader";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { cleanup, render } from "@testing-library/react";
import { useTheme } from "../../contexts/ThemeContext";
Expand All @@ -11,77 +12,106 @@ let cleanupDom: (() => void) | null = null;
let apiStatus: "auth_required" | "connecting" | "error" = "auth_required";
let apiError: string | null = "Authentication required";

// AppLoader imports App, which pulls in Lottie-based components. In happy-dom,
// lottie-web's canvas bootstrap can throw during module evaluation.
void mock.module("lottie-react", () => ({
__esModule: true,
default: () => <div data-testid="LottieMock" />,
}));

void mock.module("@/browser/contexts/API", () => ({
APIProvider: (props: { children: React.ReactNode }) => props.children,
useAPI: () => {
if (apiStatus === "auth_required") {
return {
api: null,
status: "auth_required" as const,
error: apiError,
authenticate: () => undefined,
retry: () => undefined,
};
}
const passthroughRef = <T,>(value: T): T => value;

function installAppLoaderModuleMocks() {
void mock.module("react-dnd", () => ({
DndProvider: (props: { children: React.ReactNode }) => props.children,
useDrag: () => [{ isDragging: false }, passthroughRef, () => undefined] as const,
useDrop: () => [{ isOver: false }, passthroughRef] as const,
useDragLayer: () => ({ isDragging: false, item: null, currentOffset: null }),
}));

void mock.module("react-dnd-html5-backend", () => ({
HTML5Backend: {},
getEmptyImage: () => null,
}));

// AppLoader imports App, which pulls in Lottie-based components. In happy-dom,
// lottie-web's canvas bootstrap can throw during module evaluation.
void mock.module("lottie-react", () => ({
__esModule: true,
default: () => <div data-testid="LottieMock" />,
}));

void mock.module("@/browser/contexts/API", () => ({
APIProvider: (props: { children: React.ReactNode }) => props.children,
useAPI: () => {
if (apiStatus === "auth_required") {
return {
api: null,
status: "auth_required" as const,
error: apiError,
authenticate: () => undefined,
retry: () => undefined,
};
}

if (apiStatus === "error") {
return {
api: null,
status: "error" as const,
error: apiError ?? "Connection error",
authenticate: () => undefined,
retry: () => undefined,
};
}

if (apiStatus === "error") {
return {
api: null,
status: "error" as const,
error: apiError ?? "Connection error",
status: "connecting" as const,
error: null,
authenticate: () => undefined,
retry: () => undefined,
};
}

return {
api: null,
status: "connecting" as const,
error: null,
authenticate: () => undefined,
retry: () => undefined,
};
},
}));

void mock.module("@/browser/components/LoadingScreen/LoadingScreen", () => ({
LoadingScreen: () => {
const { theme } = useTheme();
return <div data-testid="LoadingScreenMock">{theme}</div>;
},
}));

void mock.module("@/browser/components/StartupConnectionError/StartupConnectionError", () => ({
StartupConnectionError: (props: { error: string }) => (
<div data-testid="StartupConnectionErrorMock">{props.error}</div>
),
}));

void mock.module("@/browser/components/AuthTokenModal/AuthTokenModal", () => ({
// Note: Module mocks leak between bun test files.
// Export all commonly-used symbols to avoid cross-test import errors.
AuthTokenModal: (props: { error?: string | null }) => (
<div data-testid="AuthTokenModalMock">{props.error ?? "no-error"}</div>
),
getStoredAuthToken: () => null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setStoredAuthToken: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
clearStoredAuthToken: () => {},
}));

import { AppLoader } from "../AppLoader/AppLoader";
},
}));

void mock.module("../../App", () => ({
__esModule: true,
// App imports the full sidebar tree (including react-dnd) even though these auth-path tests
// only need AppLoader's pre-App branching. Keep the unit focused so cross-file DOM teardown
// cannot trip react-dnd's MutationObserver bootstrap between test files.
default: () => <div data-testid="AppMock" />,
}));

void mock.module("@/browser/components/LoadingScreen/LoadingScreen", () => ({
LoadingScreen: () => {
const { theme } = useTheme();
return <div data-testid="LoadingScreenMock">{theme}</div>;
},
}));

void mock.module("@/browser/components/StartupConnectionError/StartupConnectionError", () => ({
StartupConnectionError: (props: { error: string }) => (
<div data-testid="StartupConnectionErrorMock">{props.error}</div>
),
}));

void mock.module("@/browser/components/AuthTokenModal/AuthTokenModal", () => ({
// Note: Module mocks leak between bun test files.
// Export all commonly-used symbols to avoid cross-test import errors.
AuthTokenModal: (props: { error?: string | null }) => (
<div data-testid="AuthTokenModalMock">{props.error ?? "no-error"}</div>
),
getStoredAuthToken: () => null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setStoredAuthToken: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
clearStoredAuthToken: () => {},
}));
}

let AppLoader: typeof AppLoaderComponent;

describe("AppLoader", () => {
beforeEach(() => {
cleanupDom = installDom();
installAppLoaderModuleMocks();
// eslint-disable-next-line @typescript-eslint/no-require-imports -- module mocks must be registered before loading AppLoader in bun tests.
({ AppLoader } = require("../AppLoader/AppLoader") as {
AppLoader: typeof AppLoaderComponent;
});
});

afterEach(() => {
Expand Down
4 changes: 4 additions & 0 deletions src/browser/components/AppLoader/AppLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useWorkspaceStoreRaw, workspaceStore } from "../../stores/WorkspaceStor
import { useGitStatusStoreRaw } from "../../stores/GitStatusStore";
import { useRuntimeStatusStoreRaw } from "../../stores/RuntimeStatusStore";
import { useBackgroundBashStoreRaw } from "../../stores/BackgroundBashStore";
import { useFlowPromptStoreRaw } from "../../stores/FlowPromptStore";
import { getPRStatusStoreInstance } from "../../stores/PRStatusStore";
import { ProjectProvider, useProjectContext } from "../../contexts/ProjectContext";
import { PolicyProvider, usePolicy } from "@/browser/contexts/PolicyContext";
Expand Down Expand Up @@ -71,6 +72,7 @@ function AppLoaderInner() {
const gitStatusStore = useGitStatusStoreRaw();
const runtimeStatusStore = useRuntimeStatusStoreRaw();
const backgroundBashStore = useBackgroundBashStoreRaw();
const flowPromptStore = useFlowPromptStoreRaw();

const prefersReducedMotion = useReducedMotion();

Expand All @@ -89,6 +91,7 @@ function AppLoaderInner() {
gitStatusStore.setClient(api ?? null);
runtimeStatusStore.setClient(api ?? null);
backgroundBashStore.setClient(api ?? null);
flowPromptStore.setClient(api ?? null);
getPRStatusStoreInstance().setClient(api ?? null);

if (!workspaceContext.loading) {
Expand All @@ -113,6 +116,7 @@ function AppLoaderInner() {
gitStatusStore,
runtimeStatusStore,
backgroundBashStore,
flowPromptStore,
api,
]);

Expand Down
41 changes: 41 additions & 0 deletions src/browser/components/ChatPane/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ import {
import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
import { useAutoScroll } from "@/browser/hooks/useAutoScroll";
import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor";
import { useFlowPrompt } from "@/browser/hooks/useFlowPrompt";
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import {
useWorkspaceAggregator,
useWorkspaceUsage,
useWorkspaceStoreRaw,
type WorkspaceState,
} from "@/browser/stores/WorkspaceStore";
import { MUX_HELP_CHAT_WORKSPACE_ID } from "@/common/constants/muxChat";
import { WorkspaceMenuBar } from "../WorkspaceMenuBar/WorkspaceMenuBar";
import type { DisplayedMessage, QueuedMessage as QueuedMessageData } from "@/common/types/message";
import type { RuntimeConfig } from "@/common/types/runtime";
Expand Down Expand Up @@ -83,6 +85,10 @@ import {
normalizeQueuedMessage,
type EditingMessageState,
} from "@/browser/utils/chatEditing";
import {
FlowPromptComposerCard,
shouldShowFlowPromptComposerCard,
} from "../FlowPromptComposerCard/FlowPromptComposerCard";
import { recordSyntheticReactRenderSample } from "@/browser/utils/perf/reactProfileCollector";

// Perf e2e runs load the production bundle where React's onRender profiler callbacks may not
Expand Down Expand Up @@ -1022,6 +1028,14 @@ interface ChatInputPaneProps {

const ChatInputPane: React.FC<ChatInputPaneProps> = (props) => {
const { reviews } = props;
const flowPrompt = useFlowPrompt(props.workspaceId, props.workspaceName, props.runtimeConfig);
const [isFlowPromptCollapsed, setIsFlowPromptCollapsed] = usePersistedState(
`flowPromptComposerCollapsed:${props.workspaceId}`,
false,
{ listener: true }
);

const shouldShowFlowPromptCard = shouldShowFlowPromptComposerCard(flowPrompt.state);

return (
<div className="flex flex-col gap-2">
Expand Down Expand Up @@ -1057,6 +1071,30 @@ const ChatInputPane: React.FC<ChatInputPaneProps> = (props) => {
This agent task is queued and will start automatically when a parallel slot is available.
</div>
)}
{flowPrompt.state && shouldShowFlowPromptCard ? (
<FlowPromptComposerCard
state={flowPrompt.state}
error={flowPrompt.error}
isCollapsed={isFlowPromptCollapsed}
isUpdatingAutoSendMode={flowPrompt.isUpdatingAutoSendMode}
isSendingNow={flowPrompt.isSendingNow}
onOpen={() => {
void flowPrompt.openFlowPrompt();
}}
onDisable={() => {
void flowPrompt.disableFlowPrompt();
}}
onAutoSendModeChange={(mode) => {
void flowPrompt.updateAutoSendMode(mode);
}}
onSendNow={() => {
void flowPrompt.sendNow();
}}
onToggleCollapsed={() => {
setIsFlowPromptCollapsed((collapsed) => !collapsed);
}}
/>
) : null}
<ChatInput
key={props.workspaceId}
variant="workspace"
Expand All @@ -1078,6 +1116,9 @@ const ChatInputPane: React.FC<ChatInputPaneProps> = (props) => {
onEditLastUserMessage={props.onEditLastUserMessage}
canInterrupt={props.canInterrupt}
onReady={props.onChatInputReady}
showFlowPromptShortcutHint={
props.workspaceId !== MUX_HELP_CHAT_WORKSPACE_ID && !shouldShowFlowPromptCard
}
attachedReviews={reviews.attachedReviews}
onDetachReview={reviews.detachReview}
onDetachAllReviews={reviews.detachAllAttached}
Expand Down
Loading
Loading