authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-17 21:50:40-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-18 00:59:06-08:00
log71d8f741436b69c1adc21245af6a8fd2969e467f
treed4fc54acdc6a56cc059c5a445591129e6252d559
parentdfeb673b6de6a16fefd8b34b6d56d1e71394d900

Refactor project script persistence flow in ChatView

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 62 insertions(+), 86 deletions(-)

apps/web/src/components/ChatView.tsx+62-86
...@@ -739,48 +739,35 @@ export default function ChatView() {...@@ -739,48 +739,35 @@ export default function ChatView() {
739 },739 },
740 [activeProject, activeThread, activeThreadId, api, dispatch, gitCwd],740 [activeProject, activeThread, activeThreadId, api, dispatch, gitCwd],
741 );741 );
742 const saveProjectScript = useCallback(742 const persistProjectScripts = useCallback(
743 async (input: NewProjectScriptInput) => {743 async (input: {
744 if (!activeProject) return;744 projectId: string;
745 const nextId = nextProjectScriptId(745 projectCwd: string;
746 input.name,746 previousScripts: ProjectScript[];
747 activeProject.scripts.map((script) => script.id),747 nextScripts: ProjectScript[];
748 );748 keybinding?: string | null;
749 const nextScript: ProjectScript = {749 keybindingCommand: string;
750 id: nextId,750 }) => {
751 name: input.name,
752 command: input.command,
753 icon: input.icon,
754 runOnWorktreeCreate: input.runOnWorktreeCreate,
755 };
756 const nextScripts = input.runOnWorktreeCreate
757 ? [
758 ...activeProject.scripts.map((script) =>
759 script.runOnWorktreeCreate ? { ...script, runOnWorktreeCreate: false } : script,
760 ),
761 nextScript,
762 ]
763 : [...activeProject.scripts, nextScript];
764
765 dispatch({751 dispatch({
766 type: "SET_PROJECT_SCRIPTS",752 type: "SET_PROJECT_SCRIPTS",
767 projectId: activeProject.id,753 projectId: input.projectId,
768 scripts: nextScripts,754 scripts: input.nextScripts,
769 });755 });
770756
771 if (!isElectron || !api) return;757 if (!isElectron || !api) return;
758
772 let scriptsPersisted = false;759 let scriptsPersisted = false;
773 try {760 try {
774 const updated = await api.projects.updateScripts({761 const updated = await api.projects.updateScripts({
775 id: activeProject.id,762 id: input.projectId,
776 scripts: nextScripts,763 scripts: input.nextScripts,
777 });764 });
778 scriptsPersisted = true;765 scriptsPersisted = true;
779766
780 if (input.keybinding) {767 if (input.keybinding) {
781 const keybindingUpdate = await api.server.upsertKeybinding({768 const keybindingUpdate = await api.server.upsertKeybinding({
782 key: input.keybinding,769 key: input.keybinding,
783 command: commandForProjectScript(nextId),770 command: input.keybindingCommand,
784 });771 });
785 queryClient.setQueryData(772 queryClient.setQueryData(
786 serverQueryKeys.config(),773 serverQueryKeys.config(),
...@@ -788,7 +775,7 @@ export default function ChatView() {...@@ -788,7 +775,7 @@ export default function ChatView() {
788 current775 current
789 ? { ...current, keybindings: keybindingUpdate.keybindings }776 ? { ...current, keybindings: keybindingUpdate.keybindings }
790 : {777 : {
791 cwd: activeProject.cwd,778 cwd: input.projectCwd,
792 keybindings: keybindingUpdate.keybindings,779 keybindings: keybindingUpdate.keybindings,
793 },780 },
794 );781 );
...@@ -803,20 +790,54 @@ export default function ChatView() {...@@ -803,20 +790,54 @@ export default function ChatView() {
803 if (scriptsPersisted) {790 if (scriptsPersisted) {
804 await api.projects791 await api.projects
805 .updateScripts({792 .updateScripts({
806 id: activeProject.id,793 id: input.projectId,
807 scripts: activeProject.scripts,794 scripts: input.previousScripts,
808 })795 })
809 .catch(() => undefined);796 .catch(() => undefined);
810 }797 }
811 dispatch({798 dispatch({
812 type: "SET_PROJECT_SCRIPTS",799 type: "SET_PROJECT_SCRIPTS",
813 projectId: activeProject.id,800 projectId: input.projectId,
814 scripts: activeProject.scripts,801 scripts: input.previousScripts,
815 });802 });
816 throw error;803 throw error;
817 }804 }
818 },805 },
819 [activeProject, api, dispatch, queryClient],806 [api, dispatch, queryClient],
807 );
808 const saveProjectScript = useCallback(
809 async (input: NewProjectScriptInput) => {
810 if (!activeProject) return;
811 const nextId = nextProjectScriptId(
812 input.name,
813 activeProject.scripts.map((script) => script.id),
814 );
815 const nextScript: ProjectScript = {
816 id: nextId,
817 name: input.name,
818 command: input.command,
819 icon: input.icon,
820 runOnWorktreeCreate: input.runOnWorktreeCreate,
821 };
822 const nextScripts = input.runOnWorktreeCreate
823 ? [
824 ...activeProject.scripts.map((script) =>
825 script.runOnWorktreeCreate ? { ...script, runOnWorktreeCreate: false } : script,
826 ),
827 nextScript,
828 ]
829 : [...activeProject.scripts, nextScript];
830
831 await persistProjectScripts({
832 projectId: activeProject.id,
833 projectCwd: activeProject.cwd,
834 previousScripts: activeProject.scripts,
835 nextScripts,
836 keybinding: input.keybinding,
837 keybindingCommand: commandForProjectScript(nextId),
838 });
839 },
840 [activeProject, persistProjectScripts],
820 );841 );
821 const updateProjectScript = useCallback(842 const updateProjectScript = useCallback(
822 async (scriptId: string, input: NewProjectScriptInput) => {843 async (scriptId: string, input: NewProjectScriptInput) => {
...@@ -841,61 +862,16 @@ export default function ChatView() {...@@ -841,61 +862,16 @@ export default function ChatView() {
841 : script,862 : script,
842 );863 );
843864
844 dispatch({865 await persistProjectScripts({
845 type: "SET_PROJECT_SCRIPTS",
846 projectId: activeProject.id,866 projectId: activeProject.id,
847 scripts: nextScripts,867 projectCwd: activeProject.cwd,
868 previousScripts: activeProject.scripts,
869 nextScripts,
870 keybinding: input.keybinding,
871 keybindingCommand: commandForProjectScript(scriptId),
848 });872 });
849
850 if (!isElectron || !api) return;
851 let scriptsPersisted = false;
852 try {
853 const updated = await api.projects.updateScripts({
854 id: activeProject.id,
855 scripts: nextScripts,
856 });
857 scriptsPersisted = true;
858
859 if (input.keybinding) {
860 const keybindingUpdate = await api.server.upsertKeybinding({
861 key: input.keybinding,
862 command: commandForProjectScript(scriptId),
863 });
864 queryClient.setQueryData(
865 serverQueryKeys.config(),
866 (current: { cwd: string; keybindings: ResolvedKeybindingsConfig } | undefined) =>
867 current
868 ? { ...current, keybindings: keybindingUpdate.keybindings }
869 : {
870 cwd: activeProject.cwd,
871 keybindings: keybindingUpdate.keybindings,
872 },
873 );
874 }
875
876 dispatch({
877 type: "SET_PROJECT_SCRIPTS",
878 projectId: updated.project.id,
879 scripts: updated.project.scripts,
880 });
881 } catch (error) {
882 if (scriptsPersisted) {
883 await api.projects
884 .updateScripts({
885 id: activeProject.id,
886 scripts: activeProject.scripts,
887 })
888 .catch(() => undefined);
889 }
890 dispatch({
891 type: "SET_PROJECT_SCRIPTS",
892 projectId: activeProject.id,
893 scripts: activeProject.scripts,
894 });
895 throw error;
896 }
897 },873 },
898 [activeProject, api, dispatch, queryClient],874 [activeProject, persistProjectScripts],
899 );875 );
900876
901 const handleRuntimeModeChange = async (mode: "approval-required" | "full-access") => {877 const handleRuntimeModeChange = async (mode: "approval-required" | "full-access") => {