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
14 changes: 14 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import { usePersistComposerSettings } from "./features/app/hooks/usePersistCompo
import { useSyncSelectedDiffPath } from "./features/app/hooks/useSyncSelectedDiffPath";
import { useMenuAcceleratorController } from "./features/app/hooks/useMenuAcceleratorController";
import { useAppMenuEvents } from "./features/app/hooks/useAppMenuEvents";
import { usePlanReadyActions } from "./features/app/hooks/usePlanReadyActions";
import { useWorkspaceActions } from "./features/app/hooks/useWorkspaceActions";
import { useWorkspaceCycling } from "./features/app/hooks/useWorkspaceCycling";
import { useThreadRows } from "./features/app/hooks/useThreadRows";
Expand Down Expand Up @@ -1711,6 +1712,17 @@ function MainApp() {
],
);

const { handlePlanAccept, handlePlanSubmitChanges } = usePlanReadyActions({
activeWorkspace,
activeThreadId,
collaborationModes,
resolvedModel,
resolvedEffort,
connectWorkspace,
sendUserMessageToThread,
setSelectedCollaborationModeId,
});

const orderValue = (entry: WorkspaceInfo) =>
typeof entry.settings.sortOrder === "number"
? entry.settings.sortOrder
Expand Down Expand Up @@ -1880,6 +1892,8 @@ function MainApp() {
handleApprovalDecision,
handleApprovalRemember,
handleUserInputSubmit,
onPlanAccept: handlePlanAccept,
onPlanSubmitChanges: handlePlanSubmitChanges,
onOpenSettings: () => openSettings(),
onOpenDictationSettings: () => openSettings("dictation"),
onOpenDebug: handleDebugClick,
Expand Down
61 changes: 61 additions & 0 deletions src/features/app/components/PlanReadyFollowupMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useMemo, useState } from "react";

type PlanReadyFollowupMessageProps = {
onAccept: () => void;
onSubmitChanges: (changes: string) => void;
};

export function PlanReadyFollowupMessage({
onAccept,
onSubmitChanges,
}: PlanReadyFollowupMessageProps) {
const [changes, setChanges] = useState("");
const trimmed = useMemo(() => changes.trim(), [changes]);

return (
<div className="message request-user-input-message">
<div
className="bubble request-user-input-card"
role="group"
aria-label="Plan ready"
>
<div className="request-user-input-header">
<div className="request-user-input-title">Plan ready</div>
</div>
<div className="request-user-input-body">
<section className="request-user-input-question">
<div className="request-user-input-question-text">
Start building from this plan, or describe changes to the plan.
</div>
<textarea
className="request-user-input-notes"
placeholder="Describe what you want to change in the plan..."
value={changes}
onChange={(event) => setChanges(event.target.value)}
rows={3}
/>
</section>
</div>
<div className="request-user-input-actions">
<button
type="button"
className="plan-ready-followup-change"
onClick={() => {
if (!trimmed) {
return;
}
onSubmitChanges(trimmed);
setChanges("");
}}
disabled={!trimmed}
>
Send changes
</button>
<button type="button" className="primary" onClick={onAccept}>
Implement this plan
</button>
</div>
</div>
</div>
);
}
165 changes: 165 additions & 0 deletions src/features/app/hooks/usePlanReadyActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { useCallback } from "react";
import type { CollaborationModeOption, WorkspaceInfo } from "../../../types";
import {
makePlanReadyAcceptMessage,
makePlanReadyChangesMessage,
} from "../../../utils/internalPlanReadyMessages";

type SendUserMessageOptions = {
collaborationMode?: Record<string, unknown> | null;
};

type SendUserMessageToThread = (
workspace: WorkspaceInfo,
threadId: string,
message: string,
imageIds: string[],
options?: SendUserMessageOptions,
) => Promise<void>;

type UsePlanReadyActionsOptions = {
activeWorkspace: WorkspaceInfo | null;
activeThreadId: string | null;
collaborationModes: CollaborationModeOption[];
resolvedModel: string | null;
resolvedEffort: string | null;
connectWorkspace: (workspace: WorkspaceInfo) => Promise<void>;
sendUserMessageToThread: SendUserMessageToThread;
setSelectedCollaborationModeId: (modeId: string) => void;
};

export function usePlanReadyActions({
activeWorkspace,
activeThreadId,
collaborationModes,
resolvedModel,
resolvedEffort,
connectWorkspace,
sendUserMessageToThread,
setSelectedCollaborationModeId,
}: UsePlanReadyActionsOptions) {
const findCollaborationMode = useCallback(
(wanted: string) => {
const normalized = wanted.trim().toLowerCase();
if (!normalized) {
return null;
}
return (
collaborationModes.find(
(mode) => mode.id.trim().toLowerCase() === normalized,
) ??
collaborationModes.find(
(mode) => (mode.mode || mode.id).trim().toLowerCase() === normalized,
) ??
null
);
},
[collaborationModes],
);

const buildCollaborationModePayloadFor = useCallback(
(mode: CollaborationModeOption | null) => {
if (!mode) {
return null;
}

const modeValue = mode.mode || mode.id;
if (!modeValue) {
return null;
}

const settings: Record<string, unknown> = {
developer_instructions: mode.developerInstructions ?? null,
};

if (resolvedModel) {
settings.model = resolvedModel;
}
if (resolvedEffort !== null) {
settings.reasoning_effort = resolvedEffort;
}

return { mode: modeValue, settings };
},
[resolvedEffort, resolvedModel],
);

const handlePlanAccept = useCallback(async () => {
if (!activeWorkspace || !activeThreadId) {
return;
}

if (!activeWorkspace.connected) {
await connectWorkspace(activeWorkspace);
}

const defaultMode =
findCollaborationMode("default") ??
findCollaborationMode("code") ??
collaborationModes[0] ??
null;

if (defaultMode?.id) {
setSelectedCollaborationModeId(defaultMode.id);
}

const collaborationMode = buildCollaborationModePayloadFor(defaultMode);
await sendUserMessageToThread(
activeWorkspace,
activeThreadId,
makePlanReadyAcceptMessage(),
[],
Comment thread
Dimillian marked this conversation as resolved.
collaborationMode ? { collaborationMode } : undefined,
);
}, [
activeThreadId,
activeWorkspace,
buildCollaborationModePayloadFor,
collaborationModes,
connectWorkspace,
findCollaborationMode,
sendUserMessageToThread,
setSelectedCollaborationModeId,
]);

const handlePlanSubmitChanges = useCallback(
async (changes: string) => {
const trimmed = changes.trim();
if (!activeWorkspace || !activeThreadId || !trimmed) {
return;
}

if (!activeWorkspace.connected) {
await connectWorkspace(activeWorkspace);
}

const planMode = findCollaborationMode("plan");
if (planMode?.id) {
setSelectedCollaborationModeId(planMode.id);
}
const collaborationMode = buildCollaborationModePayloadFor(planMode);
const message = makePlanReadyChangesMessage(trimmed);
await sendUserMessageToThread(
activeWorkspace,
activeThreadId,
message,
[],
collaborationMode ? { collaborationMode } : undefined,
);
},
[
activeThreadId,
activeWorkspace,
buildCollaborationModePayloadFor,
connectWorkspace,
findCollaborationMode,
sendUserMessageToThread,
setSelectedCollaborationModeId,
],
);

return {
handlePlanAccept,
handlePlanSubmitChanges,
};
}
4 changes: 4 additions & 0 deletions src/features/layout/hooks/useLayoutNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ type LayoutNodesOptions = {
request: RequestUserInputRequest,
response: RequestUserInputResponse,
) => void;
onPlanAccept?: () => void;
onPlanSubmitChanges?: (changes: string) => void;
onOpenSettings: () => void;
onOpenDictationSettings?: () => void;
onOpenDebug: () => void;
Expand Down Expand Up @@ -549,6 +551,8 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult {
showMessageFilePath={options.showMessageFilePath}
userInputRequests={options.userInputRequests}
onUserInputSubmit={options.handleUserInputSubmit}
onPlanAccept={options.onPlanAccept}
onPlanSubmitChanges={options.onPlanSubmitChanges}
onOpenThreadLink={options.onOpenThreadLink}
isThinking={options.isProcessing}
isLoadingMessages={
Expand Down
Loading