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
21 changes: 21 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
# App mode: set both GH_TOKEN and ANTHROPIC_TOKEN for "tokens" mode.
# Leave both unset to use "baz" mode (OAuth authentication).
GH_TOKEN=
ANTHROPIC_TOKEN=

# Baz service URL
BAZ_BASE_URL=https://baz.co

# Descope authentication
DESCOPE_BASE_URL=https://api.descope.com
DESCOPE_PROJECT_ID=
DESCOPE_CLIENT_ID=

# OAuth callback port for local auth flow
OAUTH_CALLBACK_PORT=8020

# Integration client IDs
JIRA_CLIENT_ID=
LINEAR_CLIENT_ID=

# Environment
NODE_ENV=production
LOG_LEVEL=warn
125 changes: 125 additions & 0 deletions src/flows/LocalReview/LocalDiffPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { useState, useEffect } from "react";
import { Box, Text } from "ink";
import SelectInput from "ink-select-input";
import { ITEM_SELECTION_GAP, ITEM_SELECTOR } from "../../theme/symbols.js";
import {
hasStagedChanges,
getUnstagedFiles,
getStagedDiff,
getAllDiff,
} from "../../lib/git.js";

type DiffChoice = "staged" | "all";

interface SelectItem {
label: string;
value: DiffChoice;
}

interface LocalDiffPromptProps {
onReady: (diffText: string) => void;
onError: (message: string) => void;
}

const LocalDiffPrompt: React.FC<LocalDiffPromptProps> = ({
onReady,
onError,
}) => {
const [unstagedFiles, setUnstagedFiles] = useState<string[]>([]);
const [hasStaged, setHasStaged] = useState(false);
const [ready, setReady] = useState(false);

useEffect(() => {
try {
const staged = hasStagedChanges();
const unstaged = getUnstagedFiles();

if (!staged && unstaged.length === 0) {
onError(
"No changes detected. Stage some changes with `git add` and try again.",
);
return;
}

setHasStaged(staged);
setUnstagedFiles(unstaged);

// Auto-proceed if no unstaged files
if (unstaged.length === 0) {
onReady(getStagedDiff());
return;
}

// If no staged changes but there are unstaged, auto-select all
if (!staged) {
onReady(getAllDiff());
return;
}

setReady(true);
} catch (err) {
onError(err instanceof Error ? err.message : "Failed to read git state");
}
}, []);

if (!ready) {
return null;
}

const items: SelectItem[] = [
{ label: "Review only staged changes", value: "staged" },
{ label: "Review all changes (staged + unstaged)", value: "all" },
];

const handleSelect = (item: SelectItem) => {
if (item.value === "staged") {
onReady(getStagedDiff());
} else {
onReady(getAllDiff());
}
};

return (
<Box flexDirection="column">
<Box flexDirection="column" marginBottom={1}>
<Text color="yellow" bold>
The following files have unstaged changes:
</Text>
{unstagedFiles.map((file) => (
<Text key={file} color="yellow">
{" "}- {file}
</Text>
))}
</Box>

{hasStaged && (
<>
<Box marginBottom={1}>
<Text>How would you like to proceed?</Text>
</Box>

<SelectInput
items={items}
onSelect={handleSelect}
indicatorComponent={({ isSelected }) => (
<Text color={isSelected ? "green" : "gray"}>
{isSelected ? ITEM_SELECTOR : ITEM_SELECTION_GAP}
</Text>
)}
itemComponent={({ isSelected, label }) => (
<Text color={isSelected ? "cyan" : "white"}>{label}</Text>
)}
/>

<Box marginTop={1}>
<Text dimColor italic>
Use ↑↓ arrows and Enter to select
</Text>
</Box>
</>
)}
</Box>
);
};

export default LocalDiffPrompt;
146 changes: 146 additions & 0 deletions src/flows/LocalReview/LocalPullRequestReview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useState, useCallback } from "react";
import ReviewMenu, {
ReviewMenuAction,
CompletedSteps,
} from "../Review/ReviewMenu.js";
import PRChat from "../../pages/PRChat/PRChat.js";
import { IssueType, CheckoutChatRequest } from "../../models/chat.js";

const LOCAL_WALKTHROUGH_PROMPT =
"Please walk me through these changes. Start by showing me a very short description of what the changes do, followed by a brief summary of the sections. Do not include any section yet in your answer";

interface LocalPullRequestReviewProps {
diffText: string;
repoName: string;
onComplete: () => void;
onBack: () => void;
}

type State =
| ({ step: "menu" } & MenuStateData)
| ({ step: "prWalkthrough" } & MenuStateData)
| ({ step: "prChat" } & MenuStateData & { chatInput?: string })
| { step: "complete" };

interface MenuStateData {
completedSteps: CompletedSteps;
}

const LOCAL_PR_ID = "local";
const LOCAL_PR_NUMBER = 0;

const LocalPullRequestReview: React.FC<LocalPullRequestReviewProps> = ({
diffText,
repoName,
onComplete,
onBack,
}) => {
const initialCompletedSteps: CompletedSteps = {
unmetRequirements: false,
metRequirements: false,
comments: false,
prWalkthrough: false,
};

const [state, setState] = useState<State>({
step: "menu",
completedSteps: initialCompletedSteps,
});

const buildLocalChatRequest = useCallback(
(freeText: string, conversationId?: string): CheckoutChatRequest => {
return {
mode: "local",
repoName,
diff: Buffer.from(diffText).toString("base64"),
diffEncoding: "base64",
issue: {
type: IssueType.PR_WALKTHROUGH,
data: { id: LOCAL_PR_ID },
},
freeText,
conversationId,
};
},
[repoName, diffText],
);

const handleMenuAction = (action: ReviewMenuAction, input?: string) => {
if (state.step !== "menu") return;

switch (action) {
case "prWalkthrough":
setState({ ...state, step: "prWalkthrough" });
break;
case "prChat":
setState({ ...state, chatInput: input, step: "prChat" });
break;
case "finish":
setState({ step: "complete" });
onComplete();
break;
}
};

const handleBackFromWalkthrough = () => {
if (state.step !== "prWalkthrough") return;
setState({
step: "menu",
completedSteps: { ...state.completedSteps, prWalkthrough: true },
});
};

const handleBackFromChat = () => {
if (state.step !== "prChat") return;
setState({ ...state, step: "menu" });
};

const handleBackFromMenu = () => {
onBack();
};

switch (state.step) {
case "menu":
return (
<ReviewMenu
unmetRequirementsCount={0}
metRequirementsCount={0}
unresolvedCommentsCount={0}
completedSteps={state.completedSteps}
onSelect={handleMenuAction}
onBack={handleBackFromMenu}
/>
);
case "prWalkthrough":
return (
<PRChat
issueType={IssueType.PR_WALKTHROUGH}
prId={LOCAL_PR_ID}
fullRepoName={repoName}
prNumber={LOCAL_PR_NUMBER}
chatTitle="Local Changes Walkthrough"
chatDescription="Walkthrough of local changes. Press ESC to go back."
chatInput={LOCAL_WALKTHROUGH_PROMPT}
outputInitialMessage={false}
buildChatRequestOverride={buildLocalChatRequest}
onBack={handleBackFromWalkthrough}
/>
);
case "prChat":
return (
<PRChat
issueType={IssueType.PR_CHAT}
prId={LOCAL_PR_ID}
fullRepoName={repoName}
prNumber={LOCAL_PR_NUMBER}
chatInput={state.chatInput}
buildChatRequestOverride={buildLocalChatRequest}
onBack={handleBackFromChat}
/>
);
case "complete":
return null;
}
};

export default LocalPullRequestReview;
Loading