Skip to content

Commit

Permalink
feat(patches and diffs): track patch and diff result to send to the ide.
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcMcIntosh committed Feb 7, 2025
1 parent 400536a commit 86c5ce5
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { integrationsSlice } from "../features/Integrations";
import { agentUsageSlice } from "../features/AgentUsage/agentUsageSlice";
import { checkpointsSlice } from "../features/Checkpoints/checkpointsSlice";
import { checkpointsApi } from "../services/refact/checkpoints";
import { patchesAndDiffsTrackerSlice } from "../features/PatchesAndDiffsTracker/patchesAndDiffsTrackerSlice";

const tipOfTheDayPersistConfig = {
key: "totd",
Expand Down Expand Up @@ -108,6 +109,7 @@ const rootReducer = combineSlices(
userSurveySlice,
integrationsSlice,
checkpointsSlice,
patchesAndDiffsTrackerSlice,
);

const rootPersistConfig = {
Expand Down
2 changes: 2 additions & 0 deletions src/features/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { LoginPage } from "./Login";

import styles from "./App.module.css";
import classNames from "classnames";
import { usePatchesAndDiffsEventsForIDE } from "../hooks/usePatchesAndDiffEventsForIDE";

export interface AppProps {
style?: React.CSSProperties;
Expand All @@ -59,6 +60,7 @@ export const InnerApp: React.FC<AppProps> = ({ style }: AppProps) => {
const chatId = useAppSelector(selectChatId);
useEventBusForWeb();
useEventBusForApp();
usePatchesAndDiffsEventsForIDE();

const [isPaddingApplied, setIsPaddingApplied] = useState<boolean>(false);

Expand Down
145 changes: 145 additions & 0 deletions src/features/PatchesAndDiffsTracker/patchesAndDiffsTrackerSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { chatAskQuestionThunk, chatResponse } from "../Chat";
import { isAssistantMessage, isDiffResponse } from "../../events";
import { parseOrElse, partition } from "../../utils";

export type PatchMeta = {
chatId: string;
toolCallId: string;
filePath: string;
started: boolean;
completed: boolean;
};

const initialState: { patches: PatchMeta[] } = { patches: [] };

export const patchesAndDiffsTrackerSlice = createSlice({
name: "patchesAndDiffsTracker",
initialState,
reducers: {
addPatchMeta: (state, action: PayloadAction<PatchMeta>) => {
state.patches.push(action.payload);
},

removePatchMetaByFileNameIfCompleted: (
state,
action: PayloadAction<string[]>,
) => {
const next = state.patches.filter((patchMeta) => {
if (!patchMeta.completed) return true;
return !action.payload.includes(patchMeta.filePath);
});
state.patches = next;
},

setStartedByFilePaths: (state, action: PayloadAction<string[]>) => {
const next = state.patches.map((patchMeta) => {
if (action.payload.includes(patchMeta.filePath)) {
return { ...patchMeta, started: true };
} else {
return patchMeta;
}
});
state.patches = next;
},
},

extraReducers: (builder) => {
builder.addCase(chatAskQuestionThunk.pending, (state, action) => {
if (!action.meta.arg.toolsConfirmed) return state;
if (action.meta.arg.messages.length === 0) return state;
const { messages, chatId } = action.meta.arg;
const lastMessage = messages[messages.length - 1];
if (!isAssistantMessage(lastMessage)) return state;
const toolCalls = lastMessage.tool_calls;
if (!toolCalls) return state;
const patches = toolCalls.reduce<PatchMeta[]>((acc, toolCall) => {
if (toolCall.id === undefined) return acc;
if (toolCall.function.name !== "patch") return acc;
const filePath = pathFromArgString(toolCall.function.arguments);
if (!filePath) return acc;
return [
...acc,
{
chatId,
toolCallId: toolCall.id,
filePath,
started: false,
completed: false,
},
];
}, []);
state.patches.push(...patches);
});

builder.addCase(chatResponse, (state, action) => {
if (!isDiffResponse(action.payload)) return state;
const { id, tool_call_id } = action.payload;
const next = state.patches.map((patchMeta) => {
if (patchMeta.chatId !== id) return patchMeta;
if (patchMeta.toolCallId !== tool_call_id) return patchMeta;
return { ...patchMeta, completed: true };
});

state.patches = next;
});
},

selectors: {
selectUnsentPatchesFilePaths: (state) => {
const [unstarted, started] = partition(
state.patches,
(patchMeta) => patchMeta.started,
);
const unstaredFilePaths = unstarted.map(
(patchMeta) => patchMeta.filePath,
);
const startedFilePaths = started.map((patchMeta) => patchMeta.filePath);
return unstaredFilePaths.filter(
(filePath) => !startedFilePaths.includes(filePath),
);
},
selectCompletedPatchesFilePaths: (state) => {
const [incomplete, completed] = partition(
state.patches,
(patchMeta) => patchMeta.completed,
);
const incompleteFilePaths = incomplete.map(
(patchMeta) => patchMeta.filePath,
);
const completeFilePaths = completed.map(
(patchMeta) => patchMeta.filePath,
);
return completeFilePaths.filter(
(filePath) => !incompleteFilePaths.includes(filePath),
);
},

selectAllFilePaths: (state) => {
return state.patches.map((patchMeta) => patchMeta.filePath);
},
},
});

export const {
selectCompletedPatchesFilePaths,
selectUnsentPatchesFilePaths,
selectAllFilePaths,
} = patchesAndDiffsTrackerSlice.selectors;

export const { setStartedByFilePaths, removePatchMetaByFileNameIfCompleted } =
patchesAndDiffsTrackerSlice.actions;

const pathFromArgString = (argString: string) => {
const args = parseOrElse<Record<string, unknown> | null>(argString, null);
if (
args &&
typeof args === "object" &&
"path" in args &&
typeof args.path === "string"
) {
return args.path;
} else {
return null;
}
};
34 changes: 34 additions & 0 deletions src/hooks/usePatchesAndDiffEventsForIDE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect } from "react";
import {
removePatchMetaByFileNameIfCompleted,
selectCompletedPatchesFilePaths,
selectUnsentPatchesFilePaths,
setStartedByFilePaths,
} from "../features/PatchesAndDiffsTracker/patchesAndDiffsTrackerSlice";
import { useAppDispatch } from "./useAppDispatch";
import { useAppSelector } from "./useAppSelector";
import { useEventsBusForIDE } from "./useEventBusForIDE";

export function usePatchesAndDiffsEventsForIDE() {
const dispatch = useAppDispatch();
const unsent = useAppSelector(selectUnsentPatchesFilePaths);
const completed = useAppSelector(selectCompletedPatchesFilePaths);
const { startFileAnimation, stopFileAnimation } = useEventsBusForIDE();

useEffect(() => {
if (!unsent.length) return;
unsent.forEach((filePath) => {
startFileAnimation(filePath);
});

dispatch(setStartedByFilePaths(unsent));
}, [dispatch, startFileAnimation, unsent]);

useEffect(() => {
if (!completed.length) return;
completed.forEach((filePath) => {
stopFileAnimation(filePath);
});
dispatch(removePatchMetaByFileNameIfCompleted(completed));
}, [dispatch, completed, stopFileAnimation]);
}

0 comments on commit 86c5ce5

Please sign in to comment.